File: //proc/1/cwd/usr/share/rspamd/www/js/app/graph.js
/*
* Copyright (C) 2017 Vsevolod Stakhov <vsevolod@highsecure.ru>
* Copyright (C) 2017 Alexander Moisseev
*/
define(["app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "tabulator"],
(common, tabUtils, D3Evolution, D3Pie, d3, Tabulator) => {
"use strict";
const rrd_pie_config = {
cornerRadius: 2,
size: {
canvasWidth: 400,
canvasHeight: 180,
pieInnerRadius: "50%",
pieOuterRadius: "80%"
},
labels: {
outer: {
format: "none"
},
inner: {
hideWhenLessThanPercentage: 8,
offset: 0
},
},
padAngle: 0.02,
pieCenterOffset: {
x: -120,
y: 10,
},
total: {
enabled: true
},
};
const ui = {};
let prevUnit = "msg/s";
const rrdUnitColumns = ["min", "avg", "max", "last"];
function rrdColumnTitle(field, unit) {
const titles = {min: "Minimum", avg: "Average", max: "Maximum", last: "Last"};
const label = titles[field];
return label ? label + ", <span class=\"unit\">" + unit + "</span>" : "";
}
ui.draw = function (graphs, neighbours, checked_server, type) {
const graph_options = {
title: "Rspamd throughput",
width: 1060,
height: 370,
yAxisLabel: "Message rate, msg/s",
legend: {
space: 140,
entries: common.chartLegend
}
};
function initGraph() {
const graph = new D3Evolution("graph", {
...graph_options,
yScale: common.getSelector("selYScale"),
type: common.getSelector("selType"),
interpolate: common.getSelector("selInterpolate"),
convert: common.getSelector("selConvert"),
});
document.getElementById("selYScale").addEventListener("change", (e) => {
graph.yScale(e.currentTarget.value);
});
document.getElementById("selConvert").addEventListener("change", (e) => {
graph.convert(e.currentTarget.value);
});
document.getElementById("selType").addEventListener("change", (e) => {
graph.type(e.currentTarget.value);
});
document.getElementById("selInterpolate").addEventListener("change", (e) => {
graph.interpolate(e.currentTarget.value);
});
return graph;
}
function getRrdSummary(json, scaleFactor) {
const xExtents = d3.extent(d3.merge(json), (d) => d.x);
const timeInterval = xExtents[1] - xExtents[0];
let total = 0;
const rows = json.map((curr, i) => {
// Time intervals that don't have data are excluded from average calculation as d3.mean()ignores nulls
const avg = d3.mean(curr, (d) => d.y);
// To find an integral on the whole time interval we need to convert nulls to zeroes
// eslint-disable-next-line no-bitwise
const value = d3.mean(curr, (d) => Number(d.y)) * timeInterval / scaleFactor ^ 0;
const yExtents = d3.extent(curr, (d) => d.y);
total += value;
return {
label: graph_options.legend.entries[i].label,
value: value,
min: Number((yExtents[0] ?? 0).toFixed(6)),
avg: Number((avg ?? 0).toFixed(6)),
max: Number((yExtents[1] ?? 0).toFixed(6)),
last: Number((curr[curr.length - 1]?.y ?? 0).toFixed(6)),
color: graph_options.legend.entries[i].color,
};
}, []);
return {
rows: rows,
total: total
};
}
function initSummaryTable(rows, unit) {
common.tables.rrd_summary = new Tabulator("#rrd-table", {
layout: "fitColumns",
selectable: false,
index: "label",
data: rows,
columns: [
{title: "Action", field: "label", minWidth: 90},
{title: "Messages", field: "value", minWidth: 100},
{title: rrdColumnTitle("min", unit), field: "min", minWidth: 149},
{title: rrdColumnTitle("avg", unit), field: "avg", minWidth: 143},
{title: rrdColumnTitle("max", unit), field: "max", minWidth: 151},
{title: rrdColumnTitle("last", unit), field: "last", minWidth: 121},
],
rowFormatter: function (row) {
const c = row.getData().color;
if (c) row.getElement().style.setProperty("--rrd-row-color", c);
},
});
tabUtils.patchScrollIntoViewOnce();
tabUtils.stripTableholderTabindex("rrd_summary");
}
function drawRrdTable(rows, unit) {
if (Object.prototype.hasOwnProperty.call(common.tables, "rrd_summary")) {
common.tables.rrd_summary.updateData(rows);
if (unit !== prevUnit) {
rrdUnitColumns.forEach((field) => {
common.tables.rrd_summary.updateColumnDefinition(field, {
title: rrdColumnTitle(field, unit),
});
});
}
} else {
initSummaryTable(rows, unit);
}
}
function updateWidgets(data) {
let rrd_summary = {rows: []};
let unit = "msg/s";
if (data) {
// Autoranging
let scaleFactor = 1;
const yMax = d3.max(d3.merge(data), (d) => d.y);
if (yMax < 1) {
scaleFactor = 60;
unit = "msg/min";
data.forEach((s) => {
s.forEach((d) => {
if (d.y !== null) { d.y *= scaleFactor; }
});
});
}
rrd_summary = getRrdSummary(data, scaleFactor);
}
if (!graphs.rrd_pie) graphs.rrd_pie = new D3Pie("rrd-pie", rrd_pie_config);
graphs.rrd_pie.data(rrd_summary.rows);
graphs.graph.data(data);
if (unit !== prevUnit) {
graphs.graph.yAxisLabel("Message rate, " + unit);
document.querySelectorAll(".unit").forEach((el) => {
el.textContent = unit;
});
prevUnit = unit;
}
drawRrdTable(rrd_summary.rows, unit);
document.getElementById("rrd-total-value").innerHTML = rrd_summary.total;
}
if (!graphs.graph) {
graphs.graph = initGraph();
}
common.query("graph", {
success: function (req_data) {
let data = null;
const neighbours_data = req_data
.filter((d) => d.status) // filter out unavailable neighbours
.map((d) => d.data);
if (neighbours_data.length === 1) {
[data] = neighbours_data;
} else {
let time_match = true;
neighbours_data.reduce((res, curr, _, arr) => {
if ((curr[0][0].x !== res[0][0].x) ||
(curr[0][curr[0].length - 1].x !== res[0][res[0].length - 1].x)) {
time_match = false;
common.logError({
server: "Multi-server",
endpoint: "graph",
message: "Neighbours time extents do not match. " +
"Check if time is synchronized on all servers.",
errorType: "data_inconsistency"
});
arr.splice(1); // Break out of .reduce() by mutating the source array
}
return curr;
});
if (time_match) {
data = neighbours_data.reduce((res, curr) => curr.map((action, j) => action.map((d, i) => ({
x: d.x,
y: (res[j][i].y === null) ? d.y : res[j][i].y + d.y
}))));
}
}
updateWidgets(data);
},
complete: function () {
const refreshBtn = document.getElementById("refresh");
refreshBtn.disabled = false;
refreshBtn.classList.remove("disabled");
},
errorMessage: "Cannot receive throughput data",
errorOnceId: "alerted_graph_",
data: {type: type}
});
};
// Handling mouse events on overlapping elements
function setZIndex(selector, value) {
document.querySelectorAll(selector).forEach((el) => {
el.style.zIndex = value;
});
}
document.getElementById("rrd-pie").addEventListener("mouseover", () => {
setZIndex("#rrd-pie, #rrd-pie-tooltip", "200");
setZIndex("#rrd-table_toggle", "300");
});
document.getElementById("rrd-table_toggle").addEventListener("mouseover", () => {
setZIndex("#rrd-pie, #rrd-pie-tooltip, #rrd-table_toggle", "0");
});
return ui;
});