summaryrefslogtreecommitdiffstats
path: root/interface/js/app/graph.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--interface/js/app/graph.js252
1 files changed, 252 insertions, 0 deletions
diff --git a/interface/js/app/graph.js b/interface/js/app/graph.js
new file mode 100644
index 0000000..71306f4
--- /dev/null
+++ b/interface/js/app/graph.js
@@ -0,0 +1,252 @@
+/*
+ The MIT License (MIT)
+
+ Copyright (C) 2017 Vsevolod Stakhov <vsevolod@highsecure.ru>
+ Copyright (C) 2017 Alexander Moisseev
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+/* global FooTable */
+
+define(["jquery", "app/common", "d3evolution", "d3pie", "d3", "footable"],
+ ($, common, D3Evolution, D3Pie, d3) => {
+ "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";
+
+ 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", $.extend({}, graph_options, {
+ yScale: common.getSelector("selYScale"),
+ type: common.getSelector("selType"),
+ interpolate: common.getSelector("selInterpolate"),
+ convert: common.getSelector("selConvert"),
+ }));
+ $("#selYScale").change(function () {
+ graph.yScale(this.value);
+ });
+ $("#selConvert").change(function () {
+ graph.convert(this.value);
+ });
+ $("#selType").change(function () {
+ graph.type(this.value);
+ });
+ $("#selInterpolate").change(function () {
+ graph.interpolate(this.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].toFixed(6)),
+ avg: Number(avg.toFixed(6)),
+ max: Number(yExtents[1].toFixed(6)),
+ last: Number(curr[curr.length - 1].y.toFixed(6)),
+ color: graph_options.legend.entries[i].color,
+ };
+ }, []);
+
+ return {
+ rows: rows,
+ total: total
+ };
+ }
+
+ function initSummaryTable(rows, unit) {
+ common.tables.rrd_summary = FooTable.init("#rrd-table", {
+ sorting: {
+ enabled: true
+ },
+ columns: [
+ {name: "label", title: "Action"},
+ {name: "value", title: "Messages", defaultContent: ""},
+ {name: "min", title: "Minimum, <span class=\"unit\">" + unit + "</span>", defaultContent: ""},
+ {name: "avg", title: "Average, <span class=\"unit\">" + unit + "</span>", defaultContent: ""},
+ {name: "max", title: "Maximum, <span class=\"unit\">" + unit + "</span>", defaultContent: ""},
+ {name: "last", title: "Last, " + unit},
+ ],
+ rows: rows.map((curr, i) => ({
+ options: {
+ style: {
+ color: graph_options.legend.entries[i].color
+ }
+ },
+ value: curr
+ }), [])
+ });
+ }
+
+ function drawRrdTable(rows, unit) {
+ if (Object.prototype.hasOwnProperty.call(common.tables, "rrd_summary")) {
+ $.each(common.tables.rrd_summary.rows.all, (i, row) => {
+ row.val(rows[i], false, true);
+ });
+ } 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);
+ $(".unit").text(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.alertMessage("alert-error",
+ "Neighbours time extents do not match. Check if time is synchronized on all servers.");
+ 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 () { $("#refresh").removeAttr("disabled").removeClass("disabled"); },
+ errorMessage: "Cannot receive throughput data",
+ errorOnceId: "alerted_graph_",
+ data: {type: type}
+ });
+ };
+
+
+ // Handling mouse events on overlapping elements
+ $("#rrd-pie").mouseover(() => {
+ $("#rrd-pie,#rrd-pie-tooltip").css("z-index", "200");
+ $("#rrd-table_toggle").css("z-index", "300");
+ });
+ $("#rrd-table_toggle").mouseover(() => {
+ $("#rrd-pie,#rrd-pie-tooltip").css("z-index", "0");
+ $("#rrd-table_toggle").css("z-index", "0");
+ });
+
+ return ui;
+ });