mirror of
https://github.com/ntop/ntopng.git
synced 2026-05-03 09:20:10 +00:00
1583 lines
52 KiB
JavaScript
1583 lines
52 KiB
JavaScript
// 2019 - ntop.org
|
|
|
|
var schema_2_label = {};
|
|
var data_2_label = {};
|
|
var graph_i18n = {};
|
|
|
|
export function initLabelMaps(_schema_2_label, _data_2_label, _graph_i18n) {
|
|
schema_2_label = _schema_2_label;
|
|
data_2_label = _data_2_label;
|
|
graph_i18n = _graph_i18n;
|
|
};
|
|
|
|
function getSerieLabel(schema, serie, visualization, serie_index) {
|
|
var data_label = serie.label;
|
|
var new_label = data_2_label[data_label];
|
|
|
|
if(visualization && visualization.metrics_labels && visualization.metrics_labels[serie_index])
|
|
return visualization.metrics_labels[serie_index];
|
|
|
|
if(serie.ext_label) {
|
|
if(new_label)
|
|
return serie.ext_label + " (" + new_label + ")";
|
|
else
|
|
return serie.ext_label;
|
|
} else if((schema == "top:local_senders") || (schema == "top:local_receivers")) {
|
|
if(serie.ext_label)
|
|
return serie.ext_label;
|
|
else
|
|
return serie.tags.host
|
|
} else if(schema.startsWith("top:")) { // topk graphs
|
|
if(serie.tags.protocol)
|
|
return serie.tags.protocol;
|
|
else if(serie.tags.category)
|
|
return serie.tags.category;
|
|
else if(serie.tags.l4proto)
|
|
return serie.tags.l4proto;
|
|
else if(serie.tags.dscp_class)
|
|
return serie.tags.dscp_class;
|
|
else if(serie.tags.device && serie.tags.if_index) { // SNMP interface
|
|
if(serie.ext_label != "")
|
|
return serie.ext_label;
|
|
else
|
|
return "(" + serie.tags.if_index + ")";
|
|
} else if(serie.tags.device && serie.tags.port) // Flow device
|
|
return serie.tags.port;
|
|
else if(serie.tags.exporter && serie.tags.ifname) // Event exporter
|
|
return serie.tags.ifname;
|
|
else if(serie.tags.profile)
|
|
return serie.tags.profile;
|
|
else if(serie.tags.check)
|
|
return serie.tags.check;
|
|
else if(serie.tags.command)
|
|
return serie.tags.command.substring(4).toUpperCase();
|
|
} else if(data_label != "bytes") { // single series
|
|
if(serie.tags.protocol)
|
|
return serie.tags.protocol + " (" + new_label + ")";
|
|
else if(serie.tags.category)
|
|
return serie.tags.category + " (" + new_label + ")";
|
|
else if(serie.tags.device && serie.tags.if_index) // SNMP interface
|
|
return serie.ext_label + " (" + new_label + ")";
|
|
else if(serie.tags.device && serie.tags.port) // Flow device
|
|
return serie.tags.port + " (" + new_label + ")";
|
|
} else {
|
|
if(serie.tags.protocol)
|
|
return serie.tags.protocol;
|
|
else if(serie.tags.category)
|
|
return serie.tags.category;
|
|
else if(serie.tags.profile)
|
|
return serie.tags.profile;
|
|
else if(data_label == "bytes") {
|
|
if(schema.contains("volume"))
|
|
return graph_i18n.traffic_volume;
|
|
else
|
|
return graph_i18n.traffic;
|
|
}
|
|
}
|
|
|
|
if(schema_2_label[schema])
|
|
return NtopUtils.capitaliseFirstLetter(schema_2_label[schema]);
|
|
|
|
if(new_label)
|
|
return NtopUtils.capitaliseFirstLetter(new_label);
|
|
|
|
// default
|
|
return NtopUtils.capitaliseFirstLetter(data_label);
|
|
}
|
|
|
|
// Value formatter
|
|
function getValueFormatter(schema, metric_type, series, custom_formatter, stats) {
|
|
if(series && series.length && series[0].label) {
|
|
if(custom_formatter) {
|
|
var formatters = [];
|
|
|
|
if(typeof(custom_formatter) != "object")
|
|
custom_formatter = [custom_formatter];
|
|
|
|
for(var i=0; i<custom_formatter.length; i++) {
|
|
// translate function name to actual function
|
|
const functionName = custom_formatter[i].replace("NtopUtils.", "")
|
|
const formatterFunction = NtopUtils[functionName];
|
|
|
|
if(typeof formatterFunction !== "function")
|
|
console.error("Cannot find custom value formatter \"" + custom_formatter + "\"");
|
|
|
|
formatters[i] = formatterFunction;
|
|
}
|
|
|
|
return(formatters);
|
|
}
|
|
|
|
var label = series[0].label;
|
|
|
|
if(label.contains("bytes")) {
|
|
if(schema.contains("volume") || schema.contains("memory") || schema.contains("size"))
|
|
return [NtopUtils.bytesToSize, NtopUtils.bytesToSize];
|
|
else
|
|
return [NtopUtils.fbits_from_bytes, NtopUtils.bytesToSize];
|
|
} else if(label.contains("packets"))
|
|
return [NtopUtils.fpackets, NtopUtils.formatPackets];
|
|
else if(label.contains("points"))
|
|
return [NtopUtils.fpoints, NtopUtils.formatPoints];
|
|
else if(label.contains("flows")) {
|
|
var as_counter = ((metric_type === "counter") && (schema !== "custom:memory_vs_flows_hosts"));
|
|
return [as_counter ? NtopUtils.fflows : NtopUtils.formatValue, NtopUtils.formatFlows, as_counter ? NtopUtils.fflows : NtopUtils.formatFlows];
|
|
} else if(label.contains("millis") || label.contains("_ms")) {
|
|
return [NtopUtils.fmillis, NtopUtils.fmillis];
|
|
} else if(label.contains("alerts") && (metric_type === "counter")) {
|
|
return [NtopUtils.falerts, NtopUtils.falerts];
|
|
} else if(label.contains("percent")) {
|
|
return [NtopUtils.fpercent, NtopUtils.fpercent];
|
|
}
|
|
}
|
|
|
|
// fallback
|
|
if(stats && (stats.max_val < 1)) {
|
|
/* Use the float formatter to avoid having the same 0 value repeated into the scale */
|
|
return [NtopUtils.ffloat, NtopUtils.ffloat];
|
|
}
|
|
|
|
return [NtopUtils.fint,NtopUtils.fint];
|
|
}
|
|
|
|
function makeFlatLineValues(tstart, tstep, num, data) {
|
|
var t = tstart;
|
|
var values = [];
|
|
|
|
for(var i=0; i<num; i++) {
|
|
values[i] = [t, data ];
|
|
t += tstep;
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
function checkSeriesConsinstency(schema_name, count, series) {
|
|
var rv = true;
|
|
|
|
for(var i=0; i<series.length; i++) {
|
|
var data = series[i].data;
|
|
|
|
if(data.length > count) {
|
|
console.error("points mismatch: serie '" + getSerieLabel(schema_name, series[i]) +
|
|
"' has " + data.length + " points, expected " + count);
|
|
|
|
rv = false;
|
|
} else if(data.length < count) {
|
|
/* upsample */
|
|
series[i].data = upsampleSerie(data, count);
|
|
}
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
function upsampleSerie(serie, num_points) {
|
|
if(num_points <= serie.length)
|
|
return serie;
|
|
|
|
var res = [];
|
|
var intervals = num_points / serie.length;
|
|
|
|
function lerp(v0, v1, t) {
|
|
return (1 - t) * v0 + t * v1;
|
|
}
|
|
|
|
for(var i=0; i<num_points; i++) {
|
|
var index = i / intervals;
|
|
var prev_i = Math.floor(index);
|
|
var next_i = Math.min(Math.ceil(index), serie.length-1);
|
|
var t = index % 1; // fractional part
|
|
var v = lerp(serie[prev_i], serie[next_i], t);
|
|
//console.log(prev_i, next_i, t, ">>", v);
|
|
|
|
res.push(v);
|
|
}
|
|
|
|
return res.slice(0, num_points);
|
|
}
|
|
|
|
// the stacked total serie
|
|
function buildTotalSerie(data_series) {
|
|
var series = [];
|
|
|
|
for(var i=0; i<data_series.length; i++)
|
|
series.push(data_series[i].data);
|
|
|
|
return d3.transpose(series).map(function(x) {
|
|
return x.map(function(g) {
|
|
return g;
|
|
});
|
|
}).map(function(x) {return d3.sum(x);});
|
|
}
|
|
|
|
function arrayToNvSerie(serie_data, start, step) {
|
|
var values = [];
|
|
var t = start;
|
|
|
|
for(var i=0; i<serie_data.length; i++) {
|
|
values[i] = [t, serie_data[i]];
|
|
t += step;
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
// computes the difference between visual_total and total_serie
|
|
function buildOtherSerie(total_serie, visual_total) {
|
|
if(total_serie.length !== visual_total.length) {
|
|
console.warn("Total/Visual length mismatch: " + total_serie.length + " vs " + visual_total.length);
|
|
return;
|
|
}
|
|
|
|
var res = [];
|
|
var max_val = 0;
|
|
|
|
for(var i=0; i<total_serie.length; i++) {
|
|
var value = Math.max(0, total_serie[i] - visual_total[i]);
|
|
max_val = Math.max(max_val, value);
|
|
|
|
res.push(value);
|
|
}
|
|
|
|
if(max_val > 0.1)
|
|
return res;
|
|
}
|
|
|
|
function buildTimeArray(start_time, end_time, step) {
|
|
var arr = [];
|
|
|
|
for(var t=start_time; t<end_time; t+=step)
|
|
arr.push(t);
|
|
|
|
return arr;
|
|
}
|
|
|
|
function fixTimeRange(chart, params, align_step, actual_step) {
|
|
var diff_epoch = (params.epoch_end - params.epoch_begin);
|
|
var frame, align, tick_step, resolution, fmt = "%H:%M:%S";
|
|
|
|
// must be sorted by ascending max_diff
|
|
// max_diff / tick_step indicates the number of ticks, which should be <= 15
|
|
// max_diff / resolution indicates the number of actual points, which should be ~300
|
|
var range_params = [
|
|
// max_diff, resolution, x_format, alignment, tick_step
|
|
[15, 1, "%H:%M:%S", 1, 1], // <= 15 sec
|
|
[60, 1, "%H:%M:%S", 1, 5], // <= 1 min
|
|
[120, 1, "%H:%M:%S", 10, 10], // <= 2 min
|
|
[300, 1, "%H:%M:%S", 10, 30], // <= 5 min
|
|
[600, 5, "%H:%M:%S", 30, 60], // <= 10 min
|
|
[1200, 5, "%H:%M:%S", 60, 120], // <= 20 min
|
|
[3600, 10, "%H:%M:%S", 60, 300], // <= 1 h
|
|
[5400, 15, "%H:%M", 300, 900], // <= 1.5 h
|
|
[10800, 30, "%H:%M", 300, 900], // <= 3 h
|
|
[21600, 60, "%H:%M", 3600, 1800], // <= 6 h
|
|
[43200, 120, "%H:%M", 3600, 3600], // <= 12 h
|
|
[86400, 240, "%H:%M", 3600, 7200], // <= 1 d
|
|
[172800, 480, "%a, %H:%M", 3600, 14400], // <= 2 d
|
|
[604800, 1800, "%Y-%m-%d", 86400, 86400], // <= 7 d
|
|
[1209600, 3600, "%Y-%m-%d", 86400, 172800], // <= 14 d
|
|
[2678400, 7200, "%Y-%m-%d", 86400, 259200], // <= 1 m
|
|
[15768000, 14400, "%Y-%m-%d", 2678400, 1314000], // <= 6 m
|
|
[31622400, 14400, "%Y-%m-%d", 2678400, 2678400], // <= 1 y
|
|
];
|
|
|
|
for(var i=0; i<range_params.length; i++) {
|
|
var range = range_params[i];
|
|
|
|
if(diff_epoch <= range[0]) {
|
|
frame = range[0];
|
|
resolution = range[1];
|
|
fmt = range[2];
|
|
align = range[3];
|
|
tick_step = range[4];
|
|
break;
|
|
}
|
|
}
|
|
|
|
resolution = Math.max(actual_step, resolution);
|
|
|
|
if(align) {
|
|
align = (align_step && (frame != 86400) /* do not align daily traffic to avoid jumping to other RRA */) ? Math.max(align, align_step) : 1;
|
|
params.epoch_begin -= params.epoch_begin % align;
|
|
params.epoch_end -= params.epoch_end % align;
|
|
diff_epoch = (params.epoch_end - params.epoch_begin);
|
|
params.limit = Math.ceil(diff_epoch / resolution);
|
|
|
|
// align epoch end wrt params.limit
|
|
params.epoch_end += Math.ceil(diff_epoch / params.limit) * params.limit - diff_epoch;
|
|
chart.align = align;
|
|
chart.tick_step = tick_step;
|
|
} else
|
|
chart.tick_step = null;
|
|
|
|
chart.x_fmt = fmt;
|
|
}
|
|
|
|
function findActualStep(raw_step, tstart) {
|
|
if(typeof supported_steps === "object") {
|
|
if(supported_steps[raw_step]) {
|
|
var retention = supported_steps[raw_step].retention;
|
|
|
|
if(retention) {
|
|
var now_ts = Date.now() / 1000;
|
|
var delta = now_ts - tstart;
|
|
|
|
for(var i=0; i<retention.length; i++) {
|
|
var partial = raw_step * retention[i].aggregation_dp;
|
|
var tframe = partial * retention[i].retention_dp;
|
|
delta -= tframe;
|
|
|
|
if(delta <= 0)
|
|
return partial;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return raw_step;
|
|
}
|
|
|
|
function has_initial_zoom() {
|
|
return typeof NtopUtils.parseQuery(window.location.search).epoch_begin !== "undefined";
|
|
}
|
|
|
|
var current_zoom_level = (history.state) ? (history.state.zoom_level) : 0;
|
|
|
|
function canCompareBackwards(epoch_begin, epoch_end) {
|
|
var jump_duration = $("#btn-jump-time-ahead").data("duration");
|
|
var current_duration = epoch_end - epoch_begin;
|
|
return(jump_duration == current_duration);
|
|
}
|
|
|
|
export function fixJumpButtons(epoch_begin, epoch_end) {
|
|
var duration = $("#btn-jump-time-ahead").data("duration");
|
|
if((epoch_end + duration)*1000 > $.now())
|
|
$("#btn-jump-time-ahead").addClass("disabled");
|
|
else
|
|
$("#btn-jump-time-ahead").removeClass("disabled");
|
|
}
|
|
|
|
function showQuerySlow() {
|
|
$("#query-slow-alert").show();
|
|
}
|
|
|
|
function hideQuerySlow() {
|
|
$("#query-slow-alert").hide();
|
|
}
|
|
|
|
function chart_data_sum(series) {
|
|
return(series.reduce(function(acc, x) {
|
|
return(acc + x.values.reduce(
|
|
function(acc, pt) {
|
|
return(acc + pt[1] || 0);
|
|
}, 0)
|
|
)
|
|
}, 0));
|
|
}
|
|
|
|
function redrawExtraLines(chart, chart_id, extra_lines) {
|
|
/* Remove the previous extra lines */
|
|
d3.selectAll(chart_id + " line.extra-line").remove();
|
|
|
|
if(extra_lines.length > 0) {
|
|
var xValueScale = chart.xAxis.scale();
|
|
var yValueScale = chart.yAxis1.scale();
|
|
var g = d3.select(chart_id + " .stack1Wrap");
|
|
|
|
for(var i=0; i<extra_lines.length; i++) {
|
|
var d = extra_lines[i];
|
|
|
|
g.append("line")
|
|
.style("stroke", "#FF5B56")
|
|
.style("stroke-width", "2.5px")
|
|
.attr("x1", xValueScale(d[0]))
|
|
.attr("y1", yValueScale(d[2]))
|
|
.attr("x2", xValueScale(d[1]))
|
|
.attr("y2", yValueScale(d[3]))
|
|
.attr("class", "extra-line")
|
|
}
|
|
}
|
|
}
|
|
|
|
// add a new updateStackedChart function
|
|
export function attachStackedChartCallback(chart, schema_name, chart_id, zoom_reset_id, params, step,
|
|
metric_type, align_step, show_all_smooth, initial_range, ts_table_shown) {
|
|
var pending_chart_request = null;
|
|
var pending_table_request = null;
|
|
var d3_sel = d3.select(chart_id);
|
|
var $chart = $(chart_id);
|
|
var $zoom_reset = $(zoom_reset_id);
|
|
var $graph_zoom = $("#graph_zoom");
|
|
var max_interval = findActualStep(step, params.epoch_begin) * 8;
|
|
var initial_interval = (params.epoch_end - params.epoch_begin);
|
|
var is_max_zoom = (initial_interval <= max_interval);
|
|
var url = http_prefix + "/lua/rest/v2/get/timeseries/ts.lua";
|
|
var first_load = true;
|
|
var first_time_loaded = true;
|
|
var manual_trigger_extra_series = {}; // keeps track of series manually shown/hidden by the user
|
|
var datetime_format = "dd/MM/yyyy hh:mm:ss";
|
|
var max_cmp_over_total_ratio = 3; // if the comparison serie max value is too big compared to the actual chart series, hide it
|
|
var max_line_over_total_ratio = 10; // if the extra line series max value is too big compared to the actual chart series, hide them
|
|
var query_timer = null;
|
|
var seconds_before_query_slow = 6;
|
|
var query_completed = 0;
|
|
var query_was_aborted = false;
|
|
let last_known_t = null; // only set if show_unreachable is set
|
|
const visualization = chart.visualization_options || {};
|
|
chart.is_zoomed = ((current_zoom_level > 0) || has_initial_zoom());
|
|
|
|
if(!chart) return
|
|
|
|
/* Extra lines to draw into the chart. Each item is in the format [x_start, x_end, y_start, y_end] */
|
|
let extra_lines = [];
|
|
let unreachable_timestamps = {};
|
|
|
|
//var spinner = $("<img class='chart-loading-spinner' src='" + spinner_url + "'/>");
|
|
var spinner = $('<i class="chart-loading-spinner fas fa-spinner fa-lg fa-spin"></i>');
|
|
$chart.parent().css("position", "relative");
|
|
|
|
var chart_colors_full = [
|
|
"#C6D9FD",
|
|
"#90EE90",
|
|
"#69B87F",
|
|
"#94CFA4",
|
|
"#B3DEB6",
|
|
"#E5F1A6",
|
|
"#FFFCC6",
|
|
"#FEDEA5",
|
|
"#FFB97B",
|
|
"#FF8D6D",
|
|
"#E27B85"
|
|
];
|
|
|
|
var chart_colors_min = [
|
|
"#C6D9FD",
|
|
"#90EE90",
|
|
"#EE8434",
|
|
"#C95D63",
|
|
"#AE8799",
|
|
"#717EC3",
|
|
"#496DDB",
|
|
"#5A7ADE",
|
|
"#6986E1",
|
|
"#7791E4",
|
|
"#839BE6",
|
|
"#8EA4E8"
|
|
];
|
|
|
|
var split_directions_colors = [
|
|
"#C6D9FD",
|
|
"#90EE90",
|
|
"#EE8434",
|
|
"#C95D63",
|
|
"#AE8799",
|
|
"#717EC3",
|
|
"#496DDB",
|
|
"#5A7ADE",
|
|
"#6986E1",
|
|
"#7791E4",
|
|
"#839BE6",
|
|
"#8EA4E8"
|
|
];
|
|
|
|
/* This is used to show the "unreachable" label when the chart "show_unreachable"
|
|
* options is set. See the extra_lines computation below. */
|
|
function format_unreachable(formatter) {
|
|
return function(y, d) {
|
|
if(d && unreachable_timestamps[d[0]])
|
|
return(i18n_ext.unreachable_host);
|
|
|
|
// Not unreachable, use the provided formatter
|
|
return(formatter(y));
|
|
}
|
|
}
|
|
|
|
/* The default number of y points */
|
|
var num_ticks_y1 = null;
|
|
var num_ticks_y2 = null;
|
|
var domain_y1 = null;
|
|
var domain_y2 = null;
|
|
var first_run = true;
|
|
|
|
var update_chart_data = function(new_data) {
|
|
/* reset chart data so that the next transition animation will be gracefull */
|
|
d3_sel.datum([]).call(chart);
|
|
d3_sel.datum(new_data);
|
|
|
|
/* This additional refresh is needed to determine the yticks
|
|
* and domain, needed below.
|
|
* NOTE: calling transition().duration(500) is important to properly refresh
|
|
* the tooltip position. */
|
|
d3_sel.transition().duration(500).call(chart);
|
|
|
|
if(first_run) {
|
|
num_ticks_y1 = chart.yAxis1.ticks();
|
|
num_ticks_y2 = chart.yAxis2.ticks();
|
|
domain_y1 = chart.yDomain1();
|
|
domain_y2 = chart.yDomain2();
|
|
first_run = false;
|
|
}
|
|
|
|
if(metric_type === "gauge") {
|
|
var cur_domain_y1 = chart.yAxis1.scale().domain();
|
|
var cur_domain_y2 = chart.yAxis2.scale().domain();
|
|
|
|
cur_domain_y1 = cur_domain_y1[1] - cur_domain_y1[0];
|
|
cur_domain_y2 = cur_domain_y2[1] - cur_domain_y2[0];
|
|
|
|
/* If there are not enough points available, reduce the number of
|
|
* ticks to avoid repeated ticks with same integer value.
|
|
* Other solutions (documented in https://stackoverflow.com/questions/21075245/nvd3-prevent-repeated-values-on-y-axis)
|
|
* are not easily applicable in this case.
|
|
*
|
|
* NOTE: the problem should not occur when using NtopUtils.ffloat
|
|
*/
|
|
if(chart.yAxis1.tickFormat() != NtopUtils.ffloat)
|
|
chart.yAxis1.ticks(Math.min(cur_domain_y1, num_ticks_y1));
|
|
if(chart.yAxis2.tickFormat() != NtopUtils.ffloat)
|
|
chart.yAxis2.ticks(Math.min(cur_domain_y2, num_ticks_y2));
|
|
}
|
|
|
|
var y1_sum = chart_data_sum(new_data.filter(function(x) { return(x.yAxis == 1); }))
|
|
var y2_sum = chart_data_sum(new_data.filter(function(x) { return(x.yAxis == 2); }))
|
|
|
|
/* Fix negative ydomain values appearing when dataset is empty */
|
|
if(y1_sum == 0)
|
|
chart.yDomain1([0, 1]);
|
|
else
|
|
chart.yDomain1(domain_y1);
|
|
|
|
if(y2_sum == 0)
|
|
chart.yDomain2([0, 1]);
|
|
else
|
|
chart.yDomain2(domain_y2);
|
|
|
|
/* Refresh the chart */
|
|
d3_sel.call(chart);
|
|
nv.utils.windowResize(function() {
|
|
chart.update();
|
|
redrawExtraLines(chart, chart_id, extra_lines);
|
|
})
|
|
redrawExtraLines(chart, chart_id, extra_lines);
|
|
|
|
spinner.remove();
|
|
}
|
|
|
|
function isLegendDisabled(key, default_val) {
|
|
if(typeof localStorage !== "undefined") {
|
|
var val = localStorage.getItem("chart_series.disabled." + key);
|
|
|
|
if(val != null)
|
|
return(val === "true");
|
|
}
|
|
|
|
return default_val;
|
|
}
|
|
|
|
chart.legend.dispatch.on('legendClick', function(d,i) {
|
|
manual_trigger_extra_series[d.legend_key] = true;
|
|
|
|
if(typeof localStorage !== "undefined")
|
|
localStorage.setItem("chart_series.disabled." + d.legend_key, (!d.disabled) ? true : false);
|
|
});
|
|
|
|
chart.dispatch.on("zoom", function(e) {
|
|
var cur_zoom = [params.epoch_begin, params.epoch_end];
|
|
var t_start = Math.floor(e.xDomain[0]);
|
|
var t_end = Math.ceil(e.xDomain[1]);
|
|
var old_zoomed = chart.is_zoomed;
|
|
var is_user_zoom = (typeof e.is_user_zoom !== "undefined") ? e.is_user_zoom : true;
|
|
chart.is_zoomed = true;
|
|
|
|
if(chart.updateStackedChart(t_start, t_end, false, is_user_zoom)) {
|
|
if(is_user_zoom || e.push_state) {
|
|
//console.log("zoom IN!");
|
|
current_zoom_level += 1;
|
|
var url = NtopUtils.getHistoryParameters({epoch_begin: t_start, epoch_end: t_end});
|
|
history.pushState({zoom_level: current_zoom_level, range: [t_start, t_end]}, "", url);
|
|
}
|
|
|
|
chart.fixChartButtons();
|
|
} else
|
|
chart.is_zoomed = old_zoomed;
|
|
});
|
|
|
|
function updateZoom(zoom, is_user_zoom, force) {
|
|
var t_start = zoom[0];
|
|
var t_end = zoom[1];
|
|
|
|
chart.updateStackedChart(t_start, t_end, false, is_user_zoom, null, force);
|
|
chart.fixChartButtons();
|
|
}
|
|
|
|
chart.zoom_in = function() {
|
|
var cur_interval = params.epoch_end - params.epoch_begin;
|
|
|
|
if(cur_interval > 60) {
|
|
var delta = cur_interval/4;
|
|
$("#period_begin").datetimepicker("date", new Date((params.epoch_begin + delta) * 1000));
|
|
$("#period_end").datetimepicker("date", new Date((params.epoch_end - delta) * 1000));
|
|
updateChartFromPickers();
|
|
}
|
|
}
|
|
|
|
chart.zoom_out = function() {
|
|
var cur_interval = params.epoch_end - params.epoch_begin;
|
|
|
|
//if(current_zoom_level) {
|
|
// Zoom out from history
|
|
//console.log("zoom OUT");
|
|
//history.back();
|
|
//} else {
|
|
// Zoom out with fixed interval
|
|
//var delta = zoom_out_value;
|
|
var delta = cur_interval/2;
|
|
//if((params.epoch_end + delta)*1000 <= $.now())
|
|
//delta /= 2;
|
|
|
|
$("#period_begin").datetimepicker("date", new Date((params.epoch_begin - delta) * 1000));
|
|
$("#period_end").datetimepicker("date", new Date((params.epoch_end + delta) * 1000));
|
|
updateChartFromPickers();
|
|
//}
|
|
}
|
|
|
|
$chart.on('dblclick', function(event) {
|
|
if($(event.target).hasClass("nv-legend-text"))
|
|
// legend was double-clicked, keep the original behavior
|
|
return;
|
|
|
|
chart.zoom_out();
|
|
});
|
|
|
|
$zoom_reset.on("click", function() {
|
|
if(current_zoom_level) {
|
|
//console.log("zoom RESET");
|
|
history.go(-current_zoom_level);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('popstate', function(e) {
|
|
var zoom = initial_range;
|
|
//console.log("popstate: ", e.state);
|
|
|
|
if(e.state) {
|
|
zoom = e.state.range;
|
|
current_zoom_level = e.state.zoom_level;
|
|
} else
|
|
current_zoom_level = 0;
|
|
|
|
updateZoom(zoom, true, true /* force */);
|
|
});
|
|
|
|
chart.fixChartButtons = function() {
|
|
if((current_zoom_level > 0) || has_initial_zoom()) {
|
|
$graph_zoom.find(".btn-warning:not(.custom-zoom-btn)")
|
|
.addClass("initial-zoom-sel")
|
|
.removeClass("btn-warning");
|
|
$graph_zoom.find(".custom-zoom-btn").css("visibility", "visible");
|
|
|
|
var zoom_link = $graph_zoom.find(".custom-zoom-btn");
|
|
var link = zoom_link.val().replace(/&epoch_begin=.*/, "");
|
|
link += "&epoch_begin=" + params.epoch_begin + "&epoch_end=" + params.epoch_end;
|
|
zoom_link.val(link);
|
|
} else {
|
|
$graph_zoom.find(".initial-zoom-sel")
|
|
.addClass("btn-warning");
|
|
$graph_zoom.find(".custom-zoom-btn").css("visibility", "hidden");
|
|
chart.is_zoomed = false;
|
|
}
|
|
|
|
fixJumpButtons(params.epoch_begin, params.epoch_end);
|
|
|
|
if(current_zoom_level > 0)
|
|
$zoom_reset.show();
|
|
else
|
|
$zoom_reset.hide();
|
|
}
|
|
|
|
function checkQueryCompleted() {
|
|
var flows_dt = $("#chart1-flows");
|
|
var wait_num_queries = (ts_table_shown && ($("#chart1-flows").css("display") !== "none")) ? 2 : 1;
|
|
|
|
query_completed += 1;
|
|
|
|
if(query_completed >= wait_num_queries) {
|
|
if(query_timer) {
|
|
clearInterval(query_timer);
|
|
query_timer = null;
|
|
}
|
|
|
|
hideQuerySlow();
|
|
}
|
|
}
|
|
|
|
chart.queryWasAborted = function() {
|
|
return query_was_aborted;
|
|
}
|
|
|
|
chart.abortQuery = function() {
|
|
query_was_aborted = true;
|
|
|
|
if(pending_chart_request) {
|
|
pending_chart_request.abort();
|
|
chart.noData(i18n_ext.query_was_aborted);
|
|
update_chart_data([]);
|
|
}
|
|
|
|
if(pending_table_request)
|
|
pending_table_request.abort();
|
|
|
|
if(query_timer) {
|
|
clearInterval(query_timer);
|
|
query_timer = null;
|
|
}
|
|
|
|
hideQuerySlow();
|
|
}
|
|
|
|
chart.tableRequestCompleted = function() {
|
|
checkQueryCompleted();
|
|
pending_table_request = null;
|
|
}
|
|
|
|
chart.getDataUrl = function() {
|
|
var data_params = jQuery.extend({}, params);
|
|
delete data_params.zoom;
|
|
delete data_params.ts_compare;
|
|
data_params.extended = 1; /* with extended timestamps */
|
|
return url + "?" + $.param(data_params, true);
|
|
}
|
|
|
|
var old_start, old_end, old_interval;
|
|
|
|
/* Returns false if zoom update is rejected. */
|
|
chart.updateStackedChart = function (tstart, tend, no_spinner, is_user_zoom, on_load_callback, force_update) {
|
|
if(tstart) params.epoch_begin = tstart;
|
|
if(tend) params.epoch_end = tend;
|
|
const series_formatted_labels = {};
|
|
|
|
const now = Date.now() / 1000;
|
|
|
|
var cur_interval = (params.epoch_end - params.epoch_begin);
|
|
var actual_step = findActualStep(step, params.epoch_begin);
|
|
max_interval = actual_step * 6; /* host traffic 30 min */
|
|
|
|
if(cur_interval < max_interval) {
|
|
if((is_max_zoom && (cur_interval < old_interval)) && !force_update) {
|
|
old_interval = cur_interval;
|
|
return false;
|
|
}
|
|
|
|
if(!force_update) {
|
|
/* Ensure that a minimal number of points is available */
|
|
var epoch = params.epoch_begin + (params.epoch_end - params.epoch_begin) / 2;
|
|
var new_end = Math.floor(epoch + max_interval / 2);
|
|
|
|
if(new_end >= now) {
|
|
/* Only expand on the left side of the interval */
|
|
params.epoch_begin = params.epoch_end - max_interval;
|
|
} else {
|
|
params.epoch_begin = Math.floor(epoch - max_interval / 2);
|
|
params.epoch_end = Math.floor(epoch + max_interval / 2);
|
|
}
|
|
|
|
is_max_zoom = true;
|
|
chart.zoomType(null); // disable zoom
|
|
}
|
|
} else if (cur_interval > max_interval) {
|
|
is_max_zoom = false;
|
|
chart.zoomType('x'); // enable zoom
|
|
}
|
|
|
|
old_interval = cur_interval;
|
|
|
|
if(!first_load || has_initial_zoom() || force_update)
|
|
align_step = null;
|
|
fixTimeRange(chart, params, align_step, actual_step);
|
|
|
|
if(first_load)
|
|
initial_range = [params.epoch_begin, params.epoch_end];
|
|
|
|
if((old_start == params.epoch_begin) && (old_end == params.epoch_end) && (!force_update))
|
|
return false;
|
|
|
|
old_start = params.epoch_begin;
|
|
old_end = params.epoch_end;
|
|
|
|
if(pending_table_request)
|
|
pending_table_request.abort();
|
|
|
|
if(pending_chart_request)
|
|
pending_chart_request.abort();
|
|
else if(!no_spinner)
|
|
spinner.appendTo($chart.parent());
|
|
|
|
// Update datetime selection
|
|
$("#period_begin").datetimepicker("date", new Date(params.epoch_begin * 1000));
|
|
$("#period_end").datetimepicker("date", new Date(Math.min(params.epoch_end * 1000, $.now())));
|
|
|
|
if(query_timer)
|
|
clearInterval(query_timer);
|
|
|
|
query_timer = setInterval(showQuerySlow, seconds_before_query_slow * 1000);
|
|
query_completed = 0;
|
|
query_was_aborted = false;
|
|
chart.noData(i18n_ext.no_data_available);
|
|
hideQuerySlow();
|
|
|
|
var req_params = $.extend({}, params);
|
|
// skip past period comparison if a custom interval is selected
|
|
if(!canCompareBackwards(req_params.epoch_begin, req_params.epoch_end))
|
|
delete req_params.ts_compare;
|
|
|
|
/* Disable the null data filling only for the charts which support the
|
|
* "unreachable" status (unreachable reported as a 0 value instead of null). */
|
|
if(visualization.show_unreachable)
|
|
req_params.no_fill = 1;
|
|
|
|
// Load data via ajax
|
|
pending_chart_request = $.get(url, req_params, function(data) {
|
|
data = data.rsp; /* Adapts the response to the new REST API v1 */
|
|
|
|
if(data && data.error)
|
|
chart.noData(data.error);
|
|
|
|
if(!data || !data.series || !data.series.length || !checkSeriesConsinstency(schema_name, data.count, data.series)) {
|
|
update_chart_data([]);
|
|
return;
|
|
}
|
|
|
|
// Fix x axis
|
|
var tick_step = Math.ceil(chart.tick_step / data.step) * data.step;
|
|
chart.xAxis.tickValues(buildTimeArray(data.start, data.start + data.count * data.step, tick_step));
|
|
chart.xAxis.tickFormat(function(d) { return d3.time.format(chart.x_fmt)(new Date(d*1000)) });
|
|
|
|
// Adapt data
|
|
var res = [];
|
|
var series = data.series;
|
|
var total_serie;
|
|
var color_i = 0;
|
|
let time_elapsed = 1;
|
|
|
|
if(visualization.time_elapsed)
|
|
time_elapsed = visualization.time_elapsed;
|
|
|
|
var chart_colors = (series.length <= chart_colors_min.length) ? chart_colors_min : chart_colors_full;
|
|
|
|
for(var j=0; j<series.length; j++) {
|
|
var values = [];
|
|
var serie_data = series[j].data;
|
|
|
|
var t = data.start;
|
|
for(var i=0; i<serie_data.length; i++) {
|
|
values[i] = [t, serie_data[i] / time_elapsed ];
|
|
t += data.step;
|
|
}
|
|
|
|
var label = getSerieLabel(schema_name, series[j], visualization, j);
|
|
var legend_key = schema_name + ":" + label;
|
|
chart.current_step = data.step;
|
|
let serie_type = series[j].type;
|
|
let serie_color = chart_colors[color_i++]
|
|
|
|
if(!serie_type) {
|
|
if(visualization.split_directions) {
|
|
/* RX and TX directions are splitted, drow the second serie
|
|
* (TX) as a line */
|
|
serie_type = (j == 0) ? "area" : "line";
|
|
serie_color = split_directions_colors[j] || serie_color;
|
|
} else
|
|
serie_type = "area";
|
|
}
|
|
|
|
series_formatted_labels[j] = label;
|
|
|
|
res.push({
|
|
key: label,
|
|
yAxis: series[j].axis || 1,
|
|
values: values,
|
|
type: serie_type,
|
|
color: serie_color,
|
|
legend_key: legend_key,
|
|
disabled: isLegendDisabled(legend_key, false),
|
|
});
|
|
}
|
|
|
|
var visual_total = buildTotalSerie(series);
|
|
var has_full_data = false;
|
|
|
|
if(data.additional_series && data.additional_series.total) {
|
|
total_serie = data.additional_series.total;
|
|
|
|
/* Total -> Other */
|
|
var other_serie = buildOtherSerie(total_serie, visual_total);
|
|
|
|
if(other_serie) {
|
|
res.push({
|
|
key: graph_i18n.other,
|
|
yAxis: 1,
|
|
values: arrayToNvSerie(other_serie, data.start, data.step),
|
|
type: "area",
|
|
color: chart_colors[color_i++],
|
|
legend_key: "other",
|
|
disabled: isLegendDisabled("other", false),
|
|
});
|
|
|
|
has_full_data = true;
|
|
}
|
|
} else {
|
|
total_serie = visual_total;
|
|
has_full_data = !schema_name.startsWith("top:");
|
|
}
|
|
|
|
var past_serie = null;
|
|
|
|
if(data.additional_series) {
|
|
for(var key in data.additional_series) {
|
|
if(key == "total") {
|
|
// handle manually as "other" above
|
|
continue;
|
|
}
|
|
|
|
var serie_data = upsampleSerie(data.additional_series[key], data.count);
|
|
var ratio_over_total = d3.max(serie_data) / d3.max(visual_total);
|
|
var values = arrayToNvSerie(serie_data, data.start, data.step);
|
|
var is_disabled = isLegendDisabled(key, false);
|
|
past_serie = serie_data; // TODO: more reliable way to determine past serie
|
|
|
|
/* Hide comparison serie at first load if it's too high */
|
|
if((first_time_loaded || !manual_trigger_extra_series[key]) && (ratio_over_total > max_cmp_over_total_ratio))
|
|
is_disabled = true;
|
|
|
|
res.push({
|
|
key: NtopUtils.capitaliseFirstLetter(key),
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#7E91A0",
|
|
legend_key: key,
|
|
disabled: is_disabled,
|
|
});
|
|
}
|
|
}
|
|
|
|
/* Extra horizontal series */
|
|
if(visualization && visualization.extra_series) {
|
|
for(var i=0; i<visualization.extra_series.length; i++) {
|
|
var serie = visualization.extra_series[i];
|
|
|
|
if(!serie.label) {
|
|
console.warn("Missing extra_series label");
|
|
continue;
|
|
}
|
|
|
|
if(!serie.value) {
|
|
console.warn("Missing extra_series value");
|
|
continue;
|
|
}
|
|
|
|
var ratio_over_total = serie.value / d3.max(visual_total);
|
|
var is_disabled = isLegendDisabled(serie.label, false);
|
|
|
|
/* Hide the line serie at first load if it's too high */
|
|
if((first_time_loaded || !manual_trigger_extra_series[serie.label]) && (ratio_over_total > max_line_over_total_ratio))
|
|
is_disabled = true;
|
|
|
|
res.push({
|
|
key: serie.label,
|
|
yAxis: serie.axis || 1,
|
|
values: arrayToNvSerie(upsampleSerie([serie.value], data.count), data.start, data.step),
|
|
type: serie.type || "line",
|
|
color: serie.color || "red",
|
|
classed: serie.class,
|
|
legend_key: serie.label,
|
|
disabled: is_disabled,
|
|
});
|
|
}
|
|
}
|
|
|
|
if(!data.no_trend && has_full_data && (total_serie.length >= 3)) {
|
|
// Smoothed serie
|
|
/* num_smoothed_points determines the window size to use while computing rolling functions */
|
|
var num_smoothed_points = Math.min(Math.max(Math.floor(total_serie.length / 5), 3), 12);
|
|
|
|
var smooth_functions = {
|
|
//trend: [graph_i18n.trend, "#62ADF6", smooth, num_smoothed_points],
|
|
//ema: ["EMA", "#F96BFF", exponentialMovingAverageArray, {periods: num_smoothed_points}],
|
|
//sma: ["SMA", "#A900FF", simpleMovingAverageArray, {periods: num_smoothed_points}],
|
|
//rsi: ["RSI cur vs past", "#00FF5D", relativeStrengthIndexArray, {periods: num_smoothed_points}],
|
|
}
|
|
|
|
function add_smoothed_serie(fn_to_use) {
|
|
var options = smooth_functions[fn_to_use];
|
|
var smoothed;
|
|
|
|
if(fn_to_use == "rsi") {
|
|
if(!past_serie)
|
|
return;
|
|
|
|
var delta_serie = [];
|
|
for(var i=0; i<total_serie.length; i++) {
|
|
delta_serie[i] = total_serie[i] - past_serie[i];
|
|
}
|
|
smoothed = options[2](delta_serie, options[3]);
|
|
} else
|
|
smoothed = options[2](total_serie, options[3]);
|
|
|
|
// remove the first point as it's used as the base window in the rolling functions
|
|
if(smoothed[0])
|
|
delete smoothed[0];
|
|
|
|
var max_val = d3.max(smoothed);
|
|
if(max_val > 0) {
|
|
var aligned;
|
|
|
|
if((fn_to_use != "ema") && (fn_to_use != "sma") && (fn_to_use != "rsi")) {
|
|
var scale = d3.max(total_serie) / max_val;
|
|
var scaled = $.map(smoothed, function(x) { return x * scale; });
|
|
aligned = upsampleSerie(scaled, data.count);
|
|
} else {
|
|
var remaining = (data.count - smoothed.length);
|
|
var to_fill = remaining < num_smoothed_points ? remaining : num_smoothed_points;
|
|
|
|
/* Fill the initial buffering space */
|
|
for(var i=0; i<to_fill; i++)
|
|
smoothed.splice(0, 0, smoothed[0]);
|
|
|
|
aligned = upsampleSerie(smoothed, data.count);
|
|
}
|
|
|
|
if(fn_to_use == "rsi")
|
|
chart.yDomainRatioY2(1.0);
|
|
|
|
res.push({
|
|
key: options[0],
|
|
yAxis: (fn_to_use != "rsi") ? 1 : 2,
|
|
values: arrayToNvSerie(aligned, data.start, data.step),
|
|
type: "line",
|
|
classed: "line-animated",
|
|
color: options[1],
|
|
legend_key: fn_to_use,
|
|
disabled: isLegendDisabled(fn_to_use, false),
|
|
});
|
|
}
|
|
}
|
|
|
|
if(show_all_smooth) {
|
|
for(fn_to_use in smooth_functions)
|
|
add_smoothed_serie(fn_to_use);
|
|
}
|
|
}
|
|
|
|
/* Add extra lines. These are different from the extra series as
|
|
* they are simple lines, so they are not bound to an axis. */
|
|
extra_lines = [];
|
|
|
|
if((visualization.show_unreachable) && (res.length > 0)) {
|
|
var ref_serie = res[0].values;
|
|
let tok = ref_serie[0][0];
|
|
let was_unreachable = false;
|
|
unreachable_timestamps = {};
|
|
|
|
for(var i=0; i<ref_serie.length; i++) {
|
|
const is_unreachable = (ref_serie[i][1] === 0);
|
|
const tval = ref_serie[i][0];
|
|
|
|
if((ref_serie[i][1] == ref_serie[i][1]))
|
|
/* The most recent time for non NaN values */
|
|
last_known_t = tval;
|
|
|
|
if(!is_unreachable) {
|
|
if(was_unreachable)
|
|
extra_lines.push([tok, tval, 0, 0]);
|
|
|
|
tok = tval;
|
|
was_unreachable = false;
|
|
} else {
|
|
/* Change the reference serie point to null to fix interpolation issues */
|
|
ref_serie[i][1] = null;
|
|
unreachable_timestamps[tval] = true;
|
|
|
|
was_unreachable = true;
|
|
}
|
|
}
|
|
|
|
if(was_unreachable) {
|
|
const tlast = ref_serie[ref_serie.length - 1][0];
|
|
|
|
if(tlast != tok)
|
|
extra_lines.push([tok, tlast, 0, 0]);
|
|
}
|
|
}
|
|
|
|
// get the value formatter
|
|
var formatter1 = getValueFormatter(schema_name, metric_type, series.filter(function(d) { return(d.axis != 2); }), visualization.value_formatter, data.statistics);
|
|
var value_formatter = formatter1[0];
|
|
var tot_formatter = formatter1[1] || value_formatter;
|
|
var stats_formatter = formatter1[2] || value_formatter;
|
|
chart.yAxis1.tickFormat(value_formatter);
|
|
chart.yAxis1_formatter = visualization.show_unreachable ? format_unreachable(value_formatter) : value_formatter;
|
|
|
|
var second_axis_series = series.filter(function(d) { return(d.axis == 2); });
|
|
var formatter2 = getValueFormatter(schema_name, metric_type, second_axis_series, visualization.value_formatter2 || visualization.value_formatter, data.statistics);
|
|
var value_formatter2 = formatter2[0];
|
|
chart.yAxis2.tickFormat(value_formatter2);
|
|
chart.yAxis2_formatter = value_formatter2;
|
|
|
|
var stats_table = $("#ts-chart-stats");
|
|
var stats = data.statistics;
|
|
|
|
if(stats) {
|
|
if(stats.average) {
|
|
|
|
|
|
if(!visualization.split_directions) {
|
|
|
|
var values = makeFlatLineValues(data.start, data.step, data.count, stats.average);
|
|
|
|
|
|
res.push({
|
|
key: graph_i18n.avg,
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#AC9DDF",
|
|
legend_key: "avg",
|
|
disabled: isLegendDisabled("avg", true),
|
|
});
|
|
}else{
|
|
let avg_sent = makeFlatLineValues(data.start, data.step, data.count, stats.by_serie[0]["average"]);
|
|
let avg_rcvd = makeFlatLineValues(data.start, data.step, data.count, stats.by_serie[1]["average"]);
|
|
res.push({
|
|
key: graph_i18n.avg_sent,
|
|
yAxis: 1,
|
|
values: avg_sent,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#AC9DDF",
|
|
legend_key: "avg_sent",
|
|
disabled: isLegendDisabled("avg_sent", true),
|
|
});
|
|
res.push({
|
|
key: graph_i18n.avg_rcvd,
|
|
yAxis: 1,
|
|
values: avg_rcvd,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#AC9DDF",
|
|
legend_key: "avg_rcvd",
|
|
disabled: isLegendDisabled("avg_rcvd", true),
|
|
});
|
|
}
|
|
}
|
|
|
|
/*
|
|
Function used to split charts info, otherwise graphs with multiple
|
|
timeseries are going to have incorrect values
|
|
*/
|
|
function splitSeriesInfo(stats_name, cell, show_date, formatter, total) {
|
|
let val = "";
|
|
let time_elapsed = 1;
|
|
const val_formatter = (formatter ? formatter : stats_formatter)
|
|
|
|
if(visualization.time_elapsed)
|
|
time_elapsed = visualization.time_elapsed
|
|
|
|
if(visualization.first_timeseries_only) {
|
|
val = val_formatter(stats.by_serie[0][stats_name] / time_elapsed) + (show_date ? (" (" + (new Date(res[0].values[stats[stats_name + "_idx"]][0] * 1000)).format(datetime_format) + ")") : "");
|
|
} else if(visualization.split_directions && stats.by_serie && !total) {
|
|
const values = [];
|
|
|
|
/* Format each splitted info */
|
|
for(var i=0; i<series.length; i++) {
|
|
if(stats.by_serie[i])
|
|
values.push(val_formatter(stats.by_serie[i][stats_name] / time_elapsed) +
|
|
" [" + series_formatted_labels[i] + "]" +
|
|
/* Add the date */
|
|
(show_date ? (" (" + (new Date(res[i].values[stats.by_serie[i][stats_name + "_idx"] + 1][0] * 1000)).format(datetime_format) + ")") : ""));
|
|
}
|
|
|
|
/* Join them using a new line */
|
|
val = values.join("<br />");
|
|
} else
|
|
val = val_formatter(stats[stats_name] / time_elapsed) + (show_date ? (" (" + (new Date(res[0].values[stats[stats_name + "_idx"]][0] * 1000)).format(datetime_format) + ")") : "");
|
|
|
|
/* Add the string to the span */
|
|
if(val)
|
|
cell.show().find("span").html(val);
|
|
|
|
return values;
|
|
}
|
|
|
|
var total_cell = stats_table.find(".graph-val-total");
|
|
var average_cell = stats_table.find(".graph-val-average");
|
|
var min_cell = stats_table.find(".graph-val-min");
|
|
var max_cell = stats_table.find(".graph-val-max");
|
|
var perc_cell = stats_table.find(".graph-val-95percentile");
|
|
|
|
var total_cell_title = stats_table.find(".graph-val-total-title");
|
|
var average_cell_title = stats_table.find(".graph-val-average-title");
|
|
var max_cell_title = stats_table.find(".graph-val-max-title");
|
|
var min_cell_title = stats_table.find(".graph-val-min-title");
|
|
var perc_cell_title = stats_table.find(".graph-val-95percentile-title");
|
|
|
|
|
|
// fill the stats
|
|
if(stats.total || total_cell_title.is(':visible'))
|
|
splitSeriesInfo("total", total_cell_title, false, tot_formatter, true);
|
|
if(stats.average || average_cell_title.is(':visible'))
|
|
splitSeriesInfo("average", average_cell_title, false, stats_formatter);
|
|
if((stats.max_val || max_cell_title.is(':visible')) && res[0].values[stats.max_val_idx])
|
|
splitSeriesInfo("max_val", max_cell_title, true, stats_formatter);
|
|
if((stats.min_val || min_cell_title.is(':visible')) && res[0].values[stats.min_val_idx])
|
|
splitSeriesInfo("min_val", min_cell_title, true, stats_formatter);
|
|
if(stats["95th_percentile"] || perc_cell.is(':visible')) {
|
|
splitSeriesInfo("95th_percentile", perc_cell_title, false, stats_formatter);
|
|
|
|
|
|
|
|
|
|
|
|
if(!visualization.split_directions) {
|
|
/* When directions are split, hide the total stat */
|
|
var values = makeFlatLineValues(data.start, data.step, data.count, stats["95th_percentile"]);
|
|
|
|
res.push({
|
|
key: graph_i18n["95_perc"],
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#476DFF",
|
|
legend_key: "95perc",
|
|
disabled: isLegendDisabled("95perc", true),
|
|
});
|
|
}
|
|
else{
|
|
let percSent = makeFlatLineValues(data.start, data.step, data.count, stats.by_serie[0]["95th_percentile"]);
|
|
let percRcvd = makeFlatLineValues(data.start, data.step, data.count, stats.by_serie[1]["95th_percentile"]);
|
|
res.push({
|
|
key: graph_i18n["95_perc_sent"],
|
|
yAxis: 1,
|
|
values: percSent,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#476DFF",
|
|
legend_key: "95percSent",
|
|
disabled: isLegendDisabled("95percSent", true),
|
|
});
|
|
res.push({
|
|
key: graph_i18n["95_perc_rcvd"],
|
|
yAxis: 1,
|
|
values: percRcvd,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#476DFF",
|
|
legend_key: "95percRcvd",
|
|
disabled: isLegendDisabled("95percRcvd", true),
|
|
});
|
|
|
|
}
|
|
}
|
|
|
|
|
|
// fill the stats
|
|
if(stats.total || total_cell.is(':visible'))
|
|
splitSeriesInfo("total", total_cell, false, tot_formatter, true);
|
|
if(stats.average || average_cell.is(':visible'))
|
|
splitSeriesInfo("average", average_cell, false, stats_formatter);
|
|
if((stats.min_val || min_cell.is(':visible')) && res[0].values[stats.min_val_idx])
|
|
splitSeriesInfo("min_val", min_cell, true, stats_formatter);
|
|
if((stats.max_val || max_cell.is(':visible')) && res[0].values[stats.max_val_idx])
|
|
splitSeriesInfo("max_val", max_cell, true, stats_formatter);
|
|
if(stats["95th_percentile"] || perc_cell.is(':visible')) {
|
|
splitSeriesInfo("95th_percentile", perc_cell, false, stats_formatter);
|
|
}
|
|
|
|
|
|
// check if there are visible elements
|
|
//if(stats_table.find("td").filter(function(){ return $(this).css("display") != "none"; }).length > 0)
|
|
}
|
|
stats_table.show();
|
|
|
|
if(visualization.show_unreachable && last_known_t &&
|
|
(last_known_t + data.step > now) && (now < last_known_t + 2*data.step)) {
|
|
/* For the active monitoring chart, we show an additional point with the
|
|
* last value and the now timestamp as requested for
|
|
* https://github.com/ntop/ntopng/issues/3822 */
|
|
for(var j=0; j<res.length; j++) {
|
|
const serie = res[j].values;
|
|
|
|
if(serie.length > 0)
|
|
serie[serie.length] = [now, serie[serie.length - 1][1]];
|
|
}
|
|
}
|
|
|
|
var enabled_series = res.filter(function(d) { return(d.disabled !== true); });
|
|
|
|
if(second_axis_series.length > 0 || enabled_series.length == 0) {
|
|
// Enable all the series
|
|
for(var i=0; i<res.length; i++)
|
|
res[i].disabled = false;
|
|
}
|
|
|
|
if(second_axis_series.length > 0) {
|
|
// Don't allow series toggle by disabling legend clicks
|
|
chart.legend.updateState(false);
|
|
}
|
|
|
|
update_chart_data(res);
|
|
first_time_loaded = false;
|
|
|
|
if(data.source_aggregation)
|
|
$("#data-aggr-dropdown > button > span:first").html(data.source_aggregation);
|
|
}).fail(function(xhr, status, error) {
|
|
if (xhr.statusText =='abort') {
|
|
return;
|
|
}
|
|
|
|
console.error("Error while retrieving the timeseries data [" + status + "]: " + error);
|
|
chart.noData(error);
|
|
update_chart_data([]);
|
|
}).always(function(data, status, xhr) {
|
|
checkQueryCompleted();
|
|
pending_chart_request = null;
|
|
});
|
|
|
|
if(first_load) {
|
|
first_load = false;
|
|
|
|
/* Wait for page load because datatable is not instantiated yet right now */
|
|
$(function() {
|
|
var flows_dt = $("#chart1-flows").data("datatable");
|
|
if(flows_dt)
|
|
pending_table_request = flows_dt.pendingRequest;
|
|
});
|
|
} else {
|
|
var flows_dt = $("#chart1-flows");
|
|
|
|
/* Reload datatable */
|
|
if(ts_table_shown) {
|
|
/* note: flows_dt.data("datatable") will change after this call */
|
|
updateGraphsTableView(null, params);
|
|
|
|
if($("#chart1-flows").css("display") !== "none")
|
|
pending_table_request = flows_dt.data("datatable").pendingRequest;
|
|
}
|
|
}
|
|
|
|
if(typeof on_load_callback === "function")
|
|
on_load_callback(chart);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var graph_old_view = null;
|
|
var graph_old_has_nindex = null;
|
|
var graph_old_nindex_query = null;
|
|
|
|
export function tsQueryToTags(ts_query) {
|
|
return ts_query.split(",").
|
|
reduce(function(params, value) {
|
|
var pos = value.indexOf(":");
|
|
|
|
if(pos != -1) {
|
|
var k = value.slice(0, pos);
|
|
var v = value.slice(pos+1);
|
|
params[k] = v;
|
|
}
|
|
|
|
return params;
|
|
}, {});
|
|
}
|
|
|
|
/* Hide or show the timeseries table items based on the current time range */
|
|
function recheckGraphTableEntries() {
|
|
var tdiff = (graph_params.epoch_end - graph_params.epoch_begin);
|
|
var reset_selection = false;
|
|
$("#chart1-flows").show();
|
|
$("#graphs-table-selector").show();
|
|
|
|
for(let view_id in graph_table_views) {
|
|
var view = graph_table_views[view_id];
|
|
var elem = $("#" + view.html_id);
|
|
|
|
if(tdiff <= view.min_step) {
|
|
if(graph_old_view.id === view_id)
|
|
reset_selection = true;
|
|
|
|
elem.hide();
|
|
} else
|
|
elem.show();
|
|
}
|
|
|
|
/* Hide/show the headers */
|
|
var items_ul = $("#graphs-table-active-view").closest(".btn-group").find("ul:first");
|
|
|
|
items_ul.find("li.dropdown-header").each(function(idx,e) {
|
|
var next_item = $(e).nextAll("li").filter(function(idx,e) {
|
|
return(($(e).css("display") !== "none") || (!$(e).attr("data-view-id")));
|
|
}).first();
|
|
var divider = $(e).nextAll(".divider").first();
|
|
|
|
if(!next_item.attr("data-view-id")) {
|
|
$(e).hide();
|
|
divider.hide();
|
|
} else {
|
|
$(e).show();
|
|
divider.show();
|
|
}
|
|
});
|
|
|
|
if(reset_selection) {
|
|
/* Select the first available view */
|
|
var first_view = items_ul.find("li[data-view-id]").filter(function(idx,e) {
|
|
return($(e).css("display") !== "none");
|
|
}).first();
|
|
|
|
if(first_view.length)
|
|
setActiveGraphsTableView(first_view.attr("data-view-id"));
|
|
else {
|
|
$("#chart1-flows").hide();
|
|
$("#graphs-table-selector").hide();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function updateGraphsTableView(view, graph_params, has_nindex, nindex_query, per_page) {
|
|
if(view)
|
|
graph_old_view = view;
|
|
|
|
if(!recheckGraphTableEntries(graph_params)) {
|
|
/* handled by setActiveGraphsTableView */
|
|
return;
|
|
}
|
|
|
|
if(view) {
|
|
graph_old_has_nindex = has_nindex;
|
|
graph_old_nindex_query = nindex_query;
|
|
} else {
|
|
view = graph_old_view;
|
|
has_nindex = graph_old_has_nindex;
|
|
nindex_query = graph_old_nindex_query;
|
|
}
|
|
|
|
var graph_table = $("#chart1-flows");
|
|
nindex_query = nindex_query + "&begin_time_clause=" + graph_params.epoch_begin + "&end_time_clause=" + graph_params.epoch_end;
|
|
var nindex_buttons = "";
|
|
var params_obj = tsQueryToTags(graph_params.ts_query);
|
|
|
|
// TODO localize
|
|
|
|
/* Hide IP version selector when a host is selected */
|
|
if(!params_obj.host) {
|
|
nindex_buttons += '<div class="btn-group"><button class="btn btn-link dropdown-toggle" data-bs-toggle="dropdown">';
|
|
nindex_buttons += "IP Version";
|
|
nindex_buttons += '<span class="caret"></span></button><ul class="dropdown-menu" role="menu">';
|
|
nindex_buttons += '<li><a class="dropdown-item" href="#" onclick="return onGraphMenuClick(null, 4)">4</a></li>';
|
|
nindex_buttons += '<li><a class="dropdown-item" href="#" onclick="return onGraphMenuClick(null, 6)">6</a></li>';
|
|
nindex_buttons += '</span></div>';
|
|
}
|
|
|
|
if(view.columns) {
|
|
var url = http_prefix + (view.nindex_view ? "/lua/pro/get_nindex_flows.lua" : "/lua/pro/get_ts_table.lua");
|
|
|
|
var columns = view.columns.map(function(col) {
|
|
return {
|
|
title: col[1],
|
|
field: col[0],
|
|
css: {
|
|
textAlign: col[2], width: col[3],//
|
|
},
|
|
hidden: col[4] ? true : false,
|
|
};
|
|
});
|
|
|
|
columns.push({
|
|
title: i18n_ext.actions,
|
|
field: "drilldown",
|
|
css: {width: "1%", "text-align": "center"},
|
|
});
|
|
|
|
var old_dt = graph_table.data("datatable");
|
|
if(old_dt && old_dt.pendingRequest)
|
|
old_dt.pendingRequest.abort();
|
|
|
|
/* Force reinstantiation */
|
|
graph_table.removeData('datatable');
|
|
graph_table.html("");
|
|
|
|
graph_table.datatable({
|
|
title: "",
|
|
url: url,
|
|
perPage: per_page,
|
|
noResultsMessage: function() {
|
|
if(ts_chart.queryWasAborted())
|
|
return i18n_ext.query_was_aborted;
|
|
else
|
|
return i18n_ext.no_results_found;
|
|
},
|
|
post: function() {
|
|
var params = $.extend({}, graph_params);
|
|
delete params.ts_compare;
|
|
delete params.initial_point;
|
|
params.limit = 1; // TODO make specific query
|
|
// TODO change topk
|
|
// TODO disable statistics
|
|
params.detail_view = view.id;
|
|
|
|
return params;
|
|
},
|
|
loadingYOffset: 40,
|
|
columns: columns,
|
|
buttons: view.nindex_view ? [nindex_buttons, ] : [],
|
|
tableCallback: function() {
|
|
var data = this.resultset;
|
|
ts_chart.tableRequestCompleted();
|
|
|
|
if(!data) {
|
|
// error
|
|
return;
|
|
}
|
|
|
|
/* The user changed page */
|
|
if(data.currentPage > 1)
|
|
graph_table.data("has_interaction", true);
|
|
|
|
var stats_div = $("#chart1-flows-stats");
|
|
var has_drilldown = (data && data.data.some(function(row) { return row.drilldown; }));
|
|
|
|
/* Remove the drilldown column if no drilldown is available */
|
|
if(!has_drilldown)
|
|
$("table td:last-child, th:last-child", graph_table).remove();
|
|
|
|
if(data && data.totalRows > 0 && data.stats && data.stats.query_duration_msec) {
|
|
let time_elapsed = data.stats.query_duration_msec/1000.0;
|
|
if(time_elapsed < 0.1)
|
|
time_elapsed = "< 0.1"
|
|
$("#flows-query-time").html(time_elapsed);
|
|
$("#flows-processed-records").html(data.stats.num_records_processed);
|
|
stats_div.show();
|
|
} else
|
|
stats_div.hide();
|
|
}, rowCallback: function(row, row_data) {
|
|
if((typeof row_data.tags === "object") && (
|
|
(params_obj.category && (row_data.tags.category === params_obj.category)) ||
|
|
(params_obj.protocol && (row_data.tags.protocol === params_obj.protocol))
|
|
)) {
|
|
/* Highlight the row */
|
|
row.addClass("info");
|
|
}
|
|
|
|
return row;
|
|
}
|
|
});
|
|
}
|
|
}
|