]> source.dussan.org Git - rspamd.git/commitdiff
[WebUI] Rework scan results display
authormoisseev <moiseev@mezonplus.ru>
Tue, 20 Aug 2019 11:36:19 +0000 (14:36 +0300)
committermoisseev <moiseev@mezonplus.ru>
Tue, 20 Aug 2019 11:36:19 +0000 (14:36 +0300)
.eslintrc.json
interface/css/rspamd.css
interface/index.html
interface/js/app/history.js
interface/js/app/rspamd.js
interface/js/app/upload.js

index c9fa15153bedd14c3b2e89bf5292e3a8d4bec6ca..bfc3fd6a53e9c4236d40480f51bd47e013f65fe9 100644 (file)
@@ -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"],
index 9a0438d78d249e30d5dce1c0f28545350badac31..0e7c877d9334864317052f235d97bb0670e7e352 100644 (file)
@@ -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;
index cb22f3064a22b653b03f6a540477fde08477c14d..84aa211cd7349fa46149018b7d439980dea14f9f 100644 (file)
                                                                </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.&nbsp;score">Score / Req.&nbsp;score</th>
-                                                                                       <th class="col6" title="Symbols">Symbols</th>
-                                                                               </tr>
-                                                                       </thead>
-                                                               </table>
-                                                       </div>
-                                               </div>
                                        </div>
                                </div>
                                <div class="widget-box learn" style="display: none;">
                                                </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">
                                <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>
                                        </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>
index 49e228e3a97ffe1b2b00862c607f376f8d9fc62c..a7d656f27f8b83aaa1f462db33c492939975711c 100644 (file)
 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 = {
-            "&": "&amp;",
-            "<": "&lt;",
-            ">": "&gt;",
-            "\"": "&quot;",
-            "'": "&#39;",
-            "/": "&#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(",&#8203;") + more("rcpt_smtp") + "]";
-                            if (mime) {
-                                full += " ";
-                                shrt += " ";
-                            }
-                        }
-                        if (mime) {
-                            full += item.rcpt_mime.join(", ");
-                            shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",&#8203;") + 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);
                     }
                 }
             });
index 6e4de609dc15f162409249bb48764069284ecd6e..da22495c54738589b6c0d48b0846196172d87750 100644 (file)
@@ -23,7 +23,7 @@
  THE SOFTWARE.
  */
 
-/* global jQuery:false, Visibility:false */
+/* global jQuery:false, FooTable:false, Visibility:false */
 
 define(["jquery", "d3pie", "visibility", "nprogress", "stickytabs", "app/stats", "app/graph", "app/config",
     "app/symbols", "app/history", "app/upload"],
@@ -31,14 +31,25 @@ define(["jquery", "d3pie", "visibility", "nprogress", "stickytabs", "app/stats",
 function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_config,
     tab_symbols, tab_history, tab_upload) {
     "use strict";
-    // begin
+    var ui = {
+        page_size: {
+            scan: 25,
+            errors: 25,
+            history: 25
+        },
+        symbols: {
+            scan: [],
+            history: []
+        }
+    };
+
     var graphs = {};
     var tables = {};
     var neighbours = []; // list of clusters
     var checked_server = "All SERVERS";
-    var ui = {};
     var timer_id = [];
     var selData = null; // Graph's dataset selector state
+    var symbolDescriptions = {};
 
     NProgress.configure({
         minimum: 0.01,
@@ -124,16 +135,72 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
         }, 1000);
     }
 
-    // @return password
+    function drawTooltips() {
+        // Update symbol description tooltips
+        $.each(symbolDescriptions, function (key, description) {
+            $("abbr[data-sym-key=" + key + "]").tooltip({
+                placement: "bottom",
+                html: true,
+                title: description
+            });
+        });
+    }
+
     function getPassword() {
         return sessionStorage.getItem("Password");
     }
 
-    // @save credentials
+    function getSelector(id) {
+        var e = document.getElementById(id);
+        return e.options[e.selectedIndex].value;
+    }
+
+    function get_compare_function(table) {
+        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_" + table)];
+    }
+
     function saveCredentials(password) {
         sessionStorage.setItem("Password", password);
     }
 
+    function set_page_size(table, page_size, callback) {
+        var n = parseInt(page_size, 10); // HTML Input elements return string representing a number
+        if (n !== ui.page_size[table] && n > 0) {
+            ui.page_size[table] = n;
+            if (callback) {
+                return callback(n);
+            }
+        }
+        return null;
+    }
+
+    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 unix_time_format(tm) {
+        var date = new Date(tm ? tm * 1000 : 0);
+        return date.toLocaleString();
+    }
+
     function displayUI() {
         ui.query("auth", {
             success: function (neighbours_status) {
@@ -307,7 +374,7 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
         tab_config.setup(ui);
         tab_history.setup(ui, tables);
         tab_symbols.setup(ui, tables);
-        tab_upload.setup(ui);
+        tab_upload.setup(ui, tables);
         selData = tab_graph.setup();
     };
 
@@ -460,6 +527,7 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
     };
 
     ui.getPassword = getPassword;
+    ui.getSelector = getSelector;
 
     /**
      * @param {string} url - A string containing the URL to which the request is sent
@@ -536,5 +604,329 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
         }
     };
 
+    // Scan and History shared functions
+
+    ui.drawTooltips = drawTooltips;
+    ui.unix_time_format = unix_time_format;
+    ui.set_page_size = set_page_size;
+
+    ui.bindHistoryTableEventHandlers = function (table, symbolsCol) {
+        function change_symbols_order(order) {
+            $(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active");
+            var compare_function = get_compare_function(table);
+            $.each(tables[table].rows.all, function (i, row) {
+                var cell_val = sort_symbols(ui.symbols[table][i], compare_function);
+                row.cells[symbolsCol].val(cell_val, false, true);
+            });
+            drawTooltips();
+        }
+
+        $("#selSymOrder_" + table).unbind().change(function () {
+            var order = this.value;
+            change_symbols_order(order);
+        });
+        $("#" + table + "_page_size").change(function () {
+            set_page_size(table, this.value, function (n) { tables[table].pageSize(n); });
+        });
+        $(document).on("click", ".btn-sym-order-" + table + " button", function () {
+            var order = this.value;
+            $("#selSymOrder_" + table).val(order);
+            change_symbols_order(order);
+        });
+    };
+
+    ui.destroyTable = function (table) {
+        if (tables[table]) {
+            tables[table].destroy();
+            delete tables[table];
+        }
+    };
+
+
+    ui.initHistoryTable = function (rspamd, data, items, table, columns, expandFirst) {
+        /* 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[table] = FooTable.init("#historyTable_" + table, {
+            columns: columns,
+            rows: items,
+            expandFirst: expandFirst,
+            paging: {
+                enabled: true,
+                limit: 5,
+                size: ui.page_size[table]
+            },
+            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_" + table);
+                        detail_row.find(".btn-sym-" + table + "-" + order)
+                            .addClass("active").siblings().removeClass("active");
+                    }, 5);
+                }
+            }
+        });
+    };
+
+    ui.preprocess_item = function (rspamd, item) {
+        function escapeHTML(string) {
+            var htmlEscaper = /[&<>"'/`=]/g;
+            var htmlEscapes = {
+                "&": "&amp;",
+                "<": "&lt;",
+                ">": "&gt;",
+                "\"": "&quot;",
+                "'": "&#39;",
+                "/": "&#x2F;",
+                "`": "&#x60;",
+                "=": "&#x3D;"
+            };
+            return String(string).replace(htmlEscaper, function (match) {
+                return htmlEscapes[match];
+            });
+        }
+        function escape_HTML_array(arr) {
+            arr.forEach(function (d, i) { arr[i] = escapeHTML(d); });
+        }
+
+        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
+        };
+    };
+
+    ui.process_history_v2 = function (rspamd, data, table) {
+        // Display no more than rcpt_lim recipients
+        var rcpt_lim = 3;
+        var items = [];
+        var unsorted_symbols = [];
+        var compare_function = get_compare_function(table);
+
+        $("#selSymOrder_" + table + ", label[for='selSymOrder_" + table + "']").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(",&#8203;") + more("rcpt_smtp") + "]";
+                        if (mime) {
+                            full += " ";
+                            shrt += " ";
+                        }
+                    }
+                    if (mime) {
+                        full += item.rcpt_mime.join(", ");
+                        shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",&#8203;") + 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;
+                }
+
+                rspamd.preprocess_item(rspamd, 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);
+                if (table === "scan") {
+                    item.unix_time = (new Date()).getTime() / 1000;
+                }
+                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"];
+
+                if (table === "history") {
+                    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};
+    };
+
+    ui.waitForRowsDisplayed = function (table, rows_total, callback, iteration) {
+        var i = (typeof iteration === "undefined") ? 10 : iteration;
+        var num_rows = $("#historyTable_" + table + " > tbody > tr:not(.footable-detail-row)").length;
+        if (num_rows === ui.page_size[table] ||
+            num_rows === rows_total) {
+            return callback();
+        } else if (--i) {
+            setTimeout(function () {
+                ui.waitForRowsDisplayed(table, rows_total, callback, i);
+            }, 500);
+        }
+        return null;
+    };
+
     return ui;
 });
index a21d82484ee431055ce77b65e7d800f6e0c9b207..0a0640c519e13c25e5d131b55aa652b8eb167f54 100644 (file)
@@ -59,9 +59,72 @@ define(["jquery"],
                 }
             });
         }
+
+        function columns_v2() {
+            return [{
+                name: "id",
+                title: "ID",
+                style: {
+                    "font-size": "11px",
+                    "minWidth": 130,
+                    "overflow": "hidden",
+                    "textOverflow": "ellipsis",
+                    "wordBreak": "break-all",
+                    "whiteSpace": "normal"
+                }
+            }, {
+                name: "action",
+                title: "Action",
+                style: {
+                    "font-size": "11px",
+                    "minwidth": 82
+                }
+            }, {
+                name: "score",
+                title: "Score",
+                style: {
+                    "font-size": "11px",
+                    "maxWidth": 110
+                },
+                sortValue: function (val) { return Number(val.options.sortValue); }
+            }, {
+                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-scan" data-toggle="buttons">' +
+                            '<button type="button" class="btn btn-default btn-sym-scan-magnitude" value="magnitude">Magnitude</button>' +
+                            '<button type="button" class="btn btn-default btn-sym-scan-score" value="score">Value</button>' +
+                            '<button type="button" class="btn btn-default btn-sym-scan-name" value="name">Name</button>' +
+                        "</div>",
+                breakpoints: "all",
+                style: {
+                    "font-size": "11px",
+                    "width": 550,
+                    "maxWidth": 550
+                }
+            }, {
+                name: "time_real",
+                title: "Scan time",
+                breakpoints: "xs sm md",
+                style: {
+                    "font-size": "11px",
+                    "maxWidth": 72
+                },
+                sortValue: function (val) { return Number(val); }
+            }, {
+                sorted: true,
+                direction: "DESC",
+                name: "time",
+                title: "Time",
+                style: {
+                    "font-size": "11px"
+                },
+                sortValue: function (val) { return Number(val.options.sortValue); }
+            }];
+        }
+
         // @upload text
-        function scanText(rspamd, data, server) {
-            var items = [];
+        function scanText(rspamd, tables, data, server) {
             rspamd.query("checkv2", {
                 data: data,
                 params: {
@@ -72,58 +135,33 @@ define(["jquery"],
                     var json = neighbours_status[0].data;
                     if (json.action) {
                         rspamd.alertMessage("alert-success", "Data successfully scanned");
-                        var action = "";
-
-                        if (json.action === "clean" || json.action === "no action") {
-                            action = "label-success";
-                        } else if (json.action === "rewrite subject" || json.action === "add header" || json.action === "probable spam") {
-                            action = "label-warning";
-                        } else if (json.action === "spam") {
-                            action = "label-danger";
-                        }
 
-                        var score = "";
-                        if (json.score <= json.required_score) {
-                            score = "label-success";
-                        } else if (json.score >= json.required_score) {
-                            score = "label-danger";
-                        }
-                        $("<tbody id=\"tmpBody\"><tr>" +
-                            "<td><span class=\"label " + action + "\">" + json.action + "</span></td>" +
-                            "<td><span class=\"label " + score + "\">" + json.score.toFixed(2) + "/" + json.required_score.toFixed(2) + "</span></td>" +
-                            "</tr></tbody>")
-                            .insertAfter("#scanOutput thead");
-                        var sym_desc = {};
-                        var nsym = 0;
+                        var rows_total = $("#historyTable_scan > tbody > tr:not(.footable-detail-row)").length + 1;
+                        var o = rspamd.process_history_v2(rspamd, {rows:[json]}, "scan");
+                        var items = o.items;
+                        rspamd.symbols.scan.push(o.symbols[0]);
 
-                        $.each(json.symbols, function (i, item) {
-                            if (typeof item === "object") {
-                                var sym_id = "sym_" + nsym;
-                                if (item.description) {
-                                    sym_desc[sym_id] = item.description;
-                                }
-                                items.push("<div class=\"cell-overflow\" tabindex=\"1\"><abbr id=\"" + sym_id +
-                                "\">" + item.name + "</abbr>: " + item.score.toFixed(2) + "</div>");
-                                nsym++;
-                            }
-                        });
-                        $("<td/>", {
-                            id: "tmpSymbols",
-                            html: items.join("")
-                        }).appendTo("#scanResult");
-                        $("#tmpSymbols").insertAfter("#tmpBody td:last").removeAttr("id");
-                        $("#tmpBody").removeAttr("id");
-                        $("#scanResult").show();
-                        // Show tooltips
-                        $.each(sym_desc, function (k, v) {
-                            $("#" + k).tooltip({
-                                placement: "bottom",
-                                title: v
+                        if (Object.prototype.hasOwnProperty.call(tables, "scan")) {
+                            tables.scan.rows.load(items, true);
+                            // Is there a way to get an event when all rows are loaded?
+                            rspamd.waitForRowsDisplayed("scan", rows_total, function () {
+                                rspamd.drawTooltips();
+                                $("html, body").animate({
+                                    scrollTop: $("#scanResult").offset().top
+                                }, 1000);
                             });
-                        });
-                        $("html, body").animate({
-                            scrollTop: $("#scanResult").offset().top
-                        }, 1000);
+                        } else {
+                            rspamd.destroyTable("scan");
+                            // Is there a way to get an event when the table is destroyed?
+                            setTimeout(function () {
+                                rspamd.initHistoryTable(rspamd, data, items, "scan", columns_v2(), true);
+                                rspamd.waitForRowsDisplayed("scan", rows_total, function () {
+                                    $("html, body").animate({
+                                        scrollTop: $("#scanResult").offset().top
+                                    }, 1000);
+                                });
+                            }, 200);
+                        }
                     } else {
                         rspamd.alertMessage("alert-error", "Cannot scan data");
                     }
@@ -144,12 +182,19 @@ define(["jquery"],
             });
         }
 
-        ui.setup = function (rspamd) {
-            function getSelector(id) {
-                var e = document.getElementById(id);
-                return e.options[e.selectedIndex].value;
-            }
+        ui.setup = function (rspamd, tables) {
+            rspamd.set_page_size("scan", $("#scan_page_size").val());
+            rspamd.bindHistoryTableEventHandlers("scan", 3);
 
+            $("#cleanScanHistory").off("click");
+            $("#cleanScanHistory").on("click", function (e) {
+                e.preventDefault();
+                if (!confirm("Are you sure you want to clean scan history?")) { // eslint-disable-line no-alert
+                    return;
+                }
+                rspamd.destroyTable("scan");
+                rspamd.symbols.scan.length = 0;
+            });
             $("#scan button").attr("disabled", true);
             $("textarea").on("input", function () {
                 var $this = $(this);
@@ -176,9 +221,9 @@ define(["jquery"],
                     : {};
                 if ($.trim(data).length > 0) {
                     if (source === "scan") {
-                        var checked_server = getSelector("selSrv");
+                        var checked_server = rspamd.getSelector("selSrv");
                         var server = (checked_server === "All SERVERS") ? "local" : checked_server;
-                        scanText(rspamd, data, server);
+                        scanText(rspamd, tables, data, server);
                     } else {
                         uploadText(rspamd, data, source, headers);
                     }