commit def746a: [WebUI] Rework scan results display

moisseev moiseev at mezonplus.ru
Wed Aug 21 07:35:05 UTC 2019


Author: moisseev
Date: 2019-08-20 14:36:19 +0300
URL: https://github.com/rspamd/rspamd/commit/def746a5dda265e113940ee13329ad3b45efd187

[WebUI] Rework scan results display

---
 .eslintrc.json              |   2 +-
 interface/css/rspamd.css    |  12 +-
 interface/index.html        |  45 +++--
 interface/js/app/history.js | 433 +++-----------------------------------------
 interface/js/app/rspamd.js  | 404 ++++++++++++++++++++++++++++++++++++++++-
 interface/js/app/upload.js  | 161 ++++++++++------
 6 files changed, 566 insertions(+), 491 deletions(-)

diff --git a/.eslintrc.json b/.eslintrc.json
index c9fa15153..bfc3fd6a5 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -24,7 +24,7 @@
             "singleLine": { "afterColon": false }
         }],
         "max-params": ["warn", 6],
-        "max-statements": ["warn", 33],
+        "max-statements": ["warn", 44],
         "max-statements-per-line": ["error", { "max": 2 }],
         "multiline-comment-style": "off",
         "multiline-ternary": ["error", "always-multiline"],
diff --git a/interface/css/rspamd.css b/interface/css/rspamd.css
index 9a0438d78..0e7c877d9 100644
--- a/interface/css/rspamd.css
+++ b/interface/css/rspamd.css
@@ -502,17 +502,17 @@ td.maps-cell {
 }
 
 /* history table */
-#historyTable > tbody > tr > td,
-#historyTable > thead > tr > th {
+#historyTable_scan > tbody > tr > td,
+#historyTable_scan > thead > tr > th,
+#historyTable_history > tbody > tr > td,
+#historyTable_history > thead > tr > th {
     padding: 4px;
 }
-#historyTable > thead > tr > th {
+#historyTable_scan > thead > tr > th,
+#historyTable_history > thead > tr > th {
     padding-right: 20px;
 }
 
-#selSymOrder {
-    height: auto;
-}
 .widget-title-form label {
     font-size: 12px;
     font-weight: normal;
diff --git a/interface/index.html b/interface/index.html
index cb22f3064..84aa211cd 100644
--- a/interface/index.html
+++ b/interface/index.html
@@ -219,20 +219,6 @@
 								</div>
 							</form>
 						</div>
-						<div id="scanResult" style="display: none;">
-							<h4>Scan results:</h4>
-							<div class="well nomargin nopadding">
-								<table class="table table-log table-hover" id="scanOutput">
-									<thead>
-										<tr>
-											<th class="col4" title="Action">Action</th>
-											<th class="col5" title="Score / Req. score">Score / Req. score</th>
-											<th class="col6" title="Symbols">Symbols</th>
-										</tr>
-									</thead>
-								</table>
-							</div>
-						</div>
 					</div>
 				</div>
 				<div class="widget-box learn" style="display: none;">
@@ -274,6 +260,31 @@
 						</div>
 					</div>
 				</div>
+
+				<div class="widget-box">
+					<div class="widget-title">
+						<div id="scanResult" class="form-inline widget-title-form input-group-sm pull-right buttons">
+								<label for="selSymOrder_scan">Symbols order:</label>
+								<select id="selSymOrder_scan" class="form-control">
+									<option value="magnitude" selected>Score magnitude</option>
+									<option value="score">Score value</option>
+									<option value="name">Name</option>
+								</select>
+							<label for="scan_page_size">Rows per page:</label>
+							<input id="scan_page_size" class="form-control" value="25" min="1" type="number">
+							<button class="btn btn-default btn-sm" id="cleanScanHistory">
+								<i class="glyphicon glyphicon-trash"></i> Clean history
+							</button>
+						</div>
+						<span class="icon"><i class="glyphicon glyphicon-eye-open"></i></span>
+						<h5>Scan results history</h5>
+					</div>
+					<div class="widget-content nopadding">
+						<div id="scanLog">
+							<table class="table" id="historyTable_scan"></table>
+						</div>
+					</div>
+				</div>
 			</div>
 
 			<div class="tab-pane" id="history">
@@ -281,8 +292,8 @@
 				<div class="widget-box">
 					<div class="widget-title">
 						<div class="form-inline widget-title-form input-group-sm pull-right buttons">
-								<label for="selSymOrder">Symbols order:</label>
-								<select id="selSymOrder" class="form-control">
+								<label for="selSymOrder_history">Symbols order:</label>
+								<select id="selSymOrder_history" class="form-control">
 									<option value="magnitude" selected>Score magnitude</option>
 									<option value="score">Score value</option>
 									<option value="name">Name</option>
@@ -301,7 +312,7 @@
 					</div>
 					<div class="widget-content nopadding">
 						<div id="historyLog">
-							<table class="table" id="historyTable"></table>
+							<table class="table" id="historyTable_history"></table>
 						</div>
 					</div>
 				</div>
diff --git a/interface/js/app/history.js b/interface/js/app/history.js
index 49e228e3a..a7d656f27 100644
--- a/interface/js/app/history.js
+++ b/interface/js/app/history.js
@@ -27,245 +27,21 @@
 define(["jquery", "footable", "humanize"],
     function ($, _, Humanize) {
         "use strict";
-        var page_size = {
-            errors: 25,
-            history: 25
-        };
-
-        function set_page_size(n, callback) {
-            if (n !== page_size.history && n > 0) {
-                page_size.history = n;
-                if (callback) {
-                    return callback(n);
-                }
-            }
-            return null;
-        }
-
-        set_page_size($("#history_page_size").val());
-
         var ui = {};
         var prevVersion = null;
-        var htmlEscapes = {
-            "&": "&",
-            "<": "<",
-            ">": ">",
-            "\"": """,
-            "'": "'",
-            "/": "&#x2F;",
-            "`": "&#x60;",
-            "=": "&#x3D;"
-        };
-        var htmlEscaper = /[&<>"'/`=]/g;
-        var symbols = [];
-        var symbolDescriptions = {};
-
-        var escapeHTML = function (string) {
-            return String(string).replace(htmlEscaper, function (match) {
-                return htmlEscapes[match];
-            });
-        };
-
-        var escape_HTML_array = function (arr) {
-            arr.forEach(function (d, i) { arr[i] = escapeHTML(d); });
-        };
-
-        function unix_time_format(tm) {
-            var date = new Date(tm ? tm * 1000 : 0);
-            return date.toLocaleString();
-        }
-
-        function preprocess_item(item) {
-            for (var prop in item) {
-                if (!{}.hasOwnProperty.call(item, prop)) continue;
-                switch (prop) {
-                    case "rcpt_mime":
-                    case "rcpt_smtp":
-                        escape_HTML_array(item[prop]);
-                        break;
-                    case "symbols":
-                        Object.keys(item.symbols).forEach(function (key) {
-                            var sym = item.symbols[key];
-                            if (!sym.name) {
-                                sym.name = key;
-                            }
-                            sym.name = escapeHTML(sym.name);
-                            if (sym.description) {
-                                sym.description = escapeHTML(sym.description);
-                            }
-
-                            if (sym.options) {
-                                escape_HTML_array(sym.options);
-                            }
-                        });
-                        break;
-                    default:
-                        if (typeof item[prop] === "string") {
-                            item[prop] = escapeHTML(item[prop]);
-                        }
-                }
-            }
-
-            if (item.action === "clean" || item.action === "no action") {
-                item.action = "<div style='font-size:11px' class='label label-success'>" + item.action + "</div>";
-            } else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") {
-                item.action = "<div style='font-size:11px' class='label label-warning'>" + item.action + "</div>";
-            } else if (item.action === "spam" || item.action === "reject") {
-                item.action = "<div style='font-size:11px' class='label label-danger'>" + item.action + "</div>";
-            } else {
-                item.action = "<div style='font-size:11px' class='label label-info'>" + item.action + "</div>";
-            }
-
-            var score_content = (item.score < item.required_score)
-                ? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"
-                : "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>";
-
-            item.score = {
-                options: {
-                    sortValue: item.score
-                },
-                value: score_content
-            };
-        }
-
-        function getSelector(id) {
-            var e = document.getElementById(id);
-            return e.options[e.selectedIndex].value;
-        }
-
-        function get_compare_function() {
-            var compare_functions = {
-                magnitude: function (e1, e2) {
-                    return Math.abs(e2.score) - Math.abs(e1.score);
-                },
-                name: function (e1, e2) {
-                    return e1.name.localeCompare(e2.name);
-                },
-                score: function (e1, e2) {
-                    return e2.score - e1.score;
-                }
-            };
-
-            return compare_functions[getSelector("selSymOrder")];
-        }
-
-        function sort_symbols(o, compare_function) {
-            return Object.keys(o)
-                .map(function (key) {
-                    return o[key];
-                })
-                .sort(compare_function)
-                .map(function (e) { return e.str; })
-                .join("<br>\n");
-        }
-
-        function process_history_v2(data) {
-            // Display no more than rcpt_lim recipients
-            var rcpt_lim = 3;
-            var items = [];
-            var unsorted_symbols = [];
-            var compare_function = get_compare_function();
-
-            $("#selSymOrder, label[for='selSymOrder']").show();
-
-            $.each(data.rows,
-                function (i, item) {
-                    function more(p) {
-                        var l = item[p].length;
-                        return (l > rcpt_lim) ? " … (" + l + ")" : "";
-                    }
-                    function format_rcpt(smtp, mime) {
-                        var full = "";
-                        var shrt = "";
-                        if (smtp) {
-                            full = "[" + item.rcpt_smtp.join(", ") + "] ";
-                            shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",​") + more("rcpt_smtp") + "]";
-                            if (mime) {
-                                full += " ";
-                                shrt += " ";
-                            }
-                        }
-                        if (mime) {
-                            full += item.rcpt_mime.join(", ");
-                            shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",​") + more("rcpt_mime");
-                        }
-                        return {full:full, shrt:shrt};
-                    }
-
-                    function get_symbol_class(name, score) {
-                        if (name.match(/^GREYLIST$/)) {
-                            return "symbol-special";
-                        }
-
-                        if (score < 0) {
-                            return "symbol-negative";
-                        } else if (score > 0) {
-                            return "symbol-positive";
-                        }
-                        return null;
-                    }
-
-                    preprocess_item(item);
-                    Object.keys(item.symbols).forEach(function (key) {
-                        var sym = item.symbols[key];
-                        sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>';
-
-                        if (sym.description) {
-                            sym.str += '<abbr data-sym-key="' + key + '">' +
-                                sym.name + "</abbr></strong> (" + sym.score + ")</span>";
-                            // Store description for tooltip
-                            symbolDescriptions[key] = sym.description;
-                        } else {
-                            sym.str += sym.name + "</strong> (" + sym.score + ")</span>";
-                        }
-
-                        if (sym.options) {
-                            sym.str += " [" + sym.options.join(",") + "]";
-                        }
-                    });
-                    unsorted_symbols.push(item.symbols);
-                    item.symbols = sort_symbols(item.symbols, compare_function);
-                    item.time = {
-                        value: unix_time_format(item.unix_time),
-                        options: {
-                            sortValue: item.unix_time
-                        }
-                    };
-                    item.time_real = item.time_real.toFixed(3);
-                    item.id = item["message-id"];
-
-                    var rcpt = {};
-                    if (!item.rcpt_mime.length) {
-                        rcpt = format_rcpt(true, false);
-                    } else if ($(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 || $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0) {
-                        rcpt = format_rcpt(true, true);
-                    } else {
-                        rcpt = format_rcpt(false, true);
-                    }
-                    item.rcpt_mime_short = rcpt.shrt;
-                    item.rcpt_mime = rcpt.full;
-
-                    if (item.sender_mime !== item.sender_smtp) {
-                        item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime;
-                    }
-                    items.push(item);
-                });
 
-            return {items:items, symbols:unsorted_symbols};
-        }
-
-        function process_history_legacy(data) {
+        function process_history_legacy(rspamd, data) {
             var items = [];
 
             var compare = function (e1, e2) {
                 return e1.name.localeCompare(e2.name);
             };
 
-            $("#selSymOrder, label[for='selSymOrder']").hide();
+            $("#selSymOrder_history, label[for='selSymOrder_history']").hide();
 
             $.each(data, function (i, item) {
-                item.time = unix_time_format(item.unix_time);
-                preprocess_item(item);
+                item.time = rspamd.unix_time_format(item.unix_time);
+                rspamd.preprocess_item(rspamd, item);
                 item.symbols = Object.keys(item.symbols)
                     .map(function (key) {
                         return item.symbols[key];
@@ -274,7 +50,7 @@ define(["jquery", "footable", "humanize"],
                     .map(function (e) { return e.name; })
                     .join(", ");
                 item.time = {
-                    value: unix_time_format(item.unix_time),
+                    value: rspamd.unix_time_format(item.unix_time),
                     options: {
                         sortValue: item.unix_time
                     }
@@ -362,10 +138,10 @@ define(["jquery", "footable", "humanize"],
                 name: "symbols",
                 title: "Symbols<br /><br />" +
                         '<span style="font-weight:normal;">Sort by:</span><br />' +
-                        '<div class="btn-group btn-group-xs btn-sym-order" data-toggle="buttons">' +
-                            '<button type="button" class="btn btn-default btn-sym-magnitude" value="magnitude">Magnitude</button>' +
-                            '<button type="button" class="btn btn-default btn-sym-score" value="score">Value</button>' +
-                            '<button type="button" class="btn btn-default btn-sym-name" value="name">Name</button>' +
+                        '<div class="btn-group btn-group-xs btn-sym-order-history" data-toggle="buttons">' +
+                            '<button type="button" class="btn btn-default btn-sym-history-magnitude" value="magnitude">Magnitude</button>' +
+                            '<button type="button" class="btn btn-default btn-sym-history-score" value="score">Value</button>' +
+                            '<button type="button" class="btn btn-default btn-sym-history-name" value="name">Name</button>' +
                         "</div>",
                 breakpoints: "all",
                 style: {
@@ -500,17 +276,16 @@ define(["jquery", "footable", "humanize"],
             }];
         }
 
-        var process_functions = {
-            2: process_history_v2,
-            legacy: process_history_legacy
-        };
-
         var columns = {
             2: columns_v2,
             legacy: columns_legacy
         };
 
-        function process_history_data(data) {
+        function process_history_data(rspamd, data) {
+            var process_functions = {
+                2: rspamd.process_history_v2,
+                legacy: process_history_legacy
+            };
             var pf = process_functions.legacy;
 
             if (data.version) {
@@ -520,7 +295,7 @@ define(["jquery", "footable", "humanize"],
                 }
             }
 
-            return pf(data);
+            return pf(rspamd, data, "history");
         }
 
         function get_history_columns(data) {
@@ -536,136 +311,7 @@ define(["jquery", "footable", "humanize"],
             return func();
         }
 
-        function drawTooltips() {
-            // Update symbol description tooltips
-            $.each(symbolDescriptions, function (key, description) {
-                $("abbr[data-sym-key=" + key + "]").tooltip({
-                    placement: "bottom",
-                    html: true,
-                    title: description
-                });
-            });
-        }
-
-        function initHistoryTable(rspamd, tables, data, items) {
-            /* eslint-disable consistent-this, no-underscore-dangle, one-var-declaration-per-line */
-            FooTable.actionFilter = FooTable.Filtering.extend({
-                construct: function (instance) {
-                    this._super(instance);
-                    this.actions = ["reject", "add header", "greylist",
-                        "no action", "soft reject", "rewrite subject"];
-                    this.def = "Any action";
-                    this.$action = null;
-                },
-                $create: function () {
-                    this._super();
-                    var self = this, $form_grp = $("<div/>", {
-                        class: "form-group"
-                    }).append($("<label/>", {
-                        class: "sr-only",
-                        text: "Action"
-                    })).prependTo(self.$form);
-
-                    self.$action = $("<select/>", {
-                        class: "form-control"
-                    }).on("change", {
-                        self: self
-                    }, self._onStatusDropdownChanged).append(
-                        $("<option/>", {
-                            text: self.def
-                        })).appendTo($form_grp);
-
-                    $.each(self.actions, function (i, action) {
-                        self.$action.append($("<option/>").text(action));
-                    });
-                },
-                _onStatusDropdownChanged: function (e) {
-                    var self = e.data.self, selected = $(this).val();
-                    if (selected !== self.def) {
-                        if (selected === "reject") {
-                            self.addFilter("action", "reject -soft", ["action"]);
-                        } else {
-                            self.addFilter("action", selected, ["action"]);
-                        }
-                    } else {
-                        self.removeFilter("action");
-                    }
-                    self.filter();
-                },
-                draw: function () {
-                    this._super();
-                    var action = this.find("action");
-                    if (action instanceof FooTable.Filter) {
-                        if (action.query.val() === "reject -soft") {
-                            this.$action.val("reject");
-                        } else {
-                            this.$action.val(action.query.val());
-                        }
-                    } else {
-                        this.$action.val(this.def);
-                    }
-                }
-            });
-            /* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */
-
-            tables.history = FooTable.init("#historyTable", {
-                columns: get_history_columns(data),
-                rows: items,
-                paging: {
-                    enabled: true,
-                    limit: 5,
-                    size: page_size.history
-                },
-                filtering: {
-                    enabled: true,
-                    position: "left",
-                    connectors: false
-                },
-                sorting: {
-                    enabled: true
-                },
-                components: {
-                    filtering: FooTable.actionFilter
-                },
-                on: {
-                    "ready.ft.table": drawTooltips,
-                    "after.ft.sorting": drawTooltips,
-                    "after.ft.paging": drawTooltips,
-                    "after.ft.filtering": drawTooltips,
-                    "expand.ft.row": function (e, ft, row) {
-                        setTimeout(function () {
-                            var detail_row = row.$el.next();
-                            var order = getSelector("selSymOrder");
-                            detail_row.find(".btn-sym-" + order)
-                                .addClass("active").siblings().removeClass("active");
-                        }, 5);
-                    }
-                }
-            });
-        }
-
-        function destroyTable(tables, table) {
-            if (tables[table]) {
-                tables[table].destroy();
-                delete tables[table];
-            }
-        }
-
         ui.getHistory = function (rspamd, tables) {
-            function waitForRowsDisplayed(rows_total, callback, iteration) {
-                var i = (typeof iteration === "undefined") ? 10 : iteration;
-                var num_rows = $("#historyTable > tbody > tr").length;
-                if (num_rows === page_size.history ||
-                    num_rows === rows_total) {
-                    return callback();
-                } else if (--i) {
-                    setTimeout(function () {
-                        waitForRowsDisplayed(rows_total, callback, i);
-                    }, 500);
-                }
-                return null;
-            }
-
             rspamd.query("history", {
                 success: function (req_data) {
                     function differentVersions(neighbours_data) {
@@ -696,29 +342,29 @@ define(["jquery", "footable", "humanize"],
                             // Legacy version
                             data = [].concat.apply([], neighbours_data);
                         }
-                        var o = process_history_data(data);
+                        var o = process_history_data(rspamd, data);
                         var items = o.items;
-                        symbols = o.symbols;
+                        rspamd.symbols.history = o.symbols;
 
                         if (Object.prototype.hasOwnProperty.call(tables, "history") &&
                             version === prevVersion) {
                             tables.history.rows.load(items);
                             if (version) { // Non-legacy
                                 // Is there a way to get an event when all rows are loaded?
-                                waitForRowsDisplayed(items.length, function () {
-                                    drawTooltips();
+                                rspamd.waitForRowsDisplayed("history", items.length, function () {
+                                    rspamd.drawTooltips();
                                 });
                             }
                         } else {
-                            destroyTable(tables, "history");
+                            rspamd.destroyTable("history");
                             // Is there a way to get an event when the table is destroyed?
                             setTimeout(function () {
-                                initHistoryTable(rspamd, tables, data, items);
+                                rspamd.initHistoryTable(rspamd, data, items, "history", get_history_columns(data), false);
                             }, 200);
                         }
                         prevVersion = version;
                     } else {
-                        destroyTable(tables, "history");
+                        rspamd.destroyTable("history");
                     }
                 },
                 errorMessage: "Cannot receive history",
@@ -726,33 +372,14 @@ define(["jquery", "footable", "humanize"],
         };
 
         ui.setup = function (rspamd, tables) {
-            function change_symbols_order(order) {
-                $(".btn-sym-" + order).addClass("active").siblings().removeClass("active");
-                var compare_function = get_compare_function();
-                $.each(tables.history.rows.all, function (i, row) {
-                    var cell_val = sort_symbols(symbols[i], compare_function);
-                    row.cells[8].val(cell_val, false, true);
-                });
-                drawTooltips();
-            }
+            rspamd.set_page_size("history", $("#history_page_size").val());
+            rspamd.bindHistoryTableEventHandlers("history", 8);
 
             $("#updateHistory").off("click");
             $("#updateHistory").on("click", function (e) {
                 e.preventDefault();
                 ui.getHistory(rspamd, tables);
             });
-            $("#selSymOrder").unbind().change(function () {
-                var order = this.value;
-                change_symbols_order(order);
-            });
-            $("#history_page_size").change(function () {
-                set_page_size(this.value, function (n) { tables.history.pageSize(n); });
-            });
-            $(document).on("click", ".btn-sym-order button", function () {
-                var order = this.value;
-                $("#selSymOrder").val(order);
-                change_symbols_order(order);
-            });
 
             // @reset history log
             $("#resetHistory").off("click");
@@ -761,8 +388,8 @@ define(["jquery", "footable", "humanize"],
                 if (!confirm("Are you sure you want to reset history log?")) { // eslint-disable-line no-alert
                     return;
                 }
-                destroyTable(tables, "history");
-                destroyTable(tables, "errors");
+                rspamd.destroyTable("history");
+                rspamd.destroyTable("errors");
 
                 rspamd.query("historyreset", {
                     success: function () {
@@ -774,7 +401,7 @@ define(["jquery", "footable", "humanize"],
             });
         };
 
-        function initErrorsTable(tables, rows) {
+        function initErrorsTable(rspamd, tables, rows) {
             tables.errors = FooTable.init("#errorsLog", {
                 columns: [
                     {sorted:true, direction:"DESC", name:"ts", title:"Time", style:{"font-size":"11px", "width":300, "maxWidth":300}},
@@ -788,7 +415,7 @@ define(["jquery", "footable", "humanize"],
                 paging: {
                     enabled: true,
                     limit: 5,
-                    size: page_size.errors
+                    size: rspamd.page_size.errors
                 },
                 filtering: {
                     enabled: true,
@@ -816,7 +443,7 @@ define(["jquery", "footable", "humanize"],
                     var rows = [].concat.apply([], neighbours_data);
                     $.each(rows, function (i, item) {
                         item.ts = {
-                            value: unix_time_format(item.ts),
+                            value: rspamd.unix_time_format(item.ts),
                             options: {
                                 sortValue: item.ts
                             }
@@ -825,7 +452,7 @@ define(["jquery", "footable", "humanize"],
                     if (Object.prototype.hasOwnProperty.call(tables, "errors")) {
                         tables.errors.rows.load(rows);
                     } else {
-                        initErrorsTable(tables, rows);
+                        initErrorsTable(rspamd, tables, rows);
                     }
                 }
             });
*** OUTPUT TRUNCATED, 662 LINES SKIPPED ***


More information about the Commits mailing list