diff options
author | moisseev <moiseev@mezonplus.ru> | 2019-08-20 14:36:19 +0300 |
---|---|---|
committer | moisseev <moiseev@mezonplus.ru> | 2019-08-20 14:36:19 +0300 |
commit | def746a5dda265e113940ee13329ad3b45efd187 (patch) | |
tree | 8fff4fd7e7c734ef469b86667a67153d9d4c1484 /interface | |
parent | 622323beb53c4e465b256dd38afe7abb75a93ee4 (diff) | |
download | rspamd-def746a5dda265e113940ee13329ad3b45efd187.tar.gz rspamd-def746a5dda265e113940ee13329ad3b45efd187.zip |
[WebUI] Rework scan results display
Diffstat (limited to 'interface')
-rw-r--r-- | interface/css/rspamd.css | 12 | ||||
-rw-r--r-- | interface/index.html | 45 | ||||
-rw-r--r-- | interface/js/app/history.js | 433 | ||||
-rw-r--r-- | interface/js/app/rspamd.js | 404 | ||||
-rw-r--r-- | interface/js/app/upload.js | 161 |
5 files changed, 565 insertions, 490 deletions
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 = { - "&": "&", - "<": "<", - ">": ">", - "\"": """, - "'": "'", - "/": "/", - "`": "`", - "=": "=" - }; - 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); } } }); diff --git a/interface/js/app/rspamd.js b/interface/js/app/rspamd.js index 6e4de609d..da22495c5 100644 --- a/interface/js/app/rspamd.js +++ b/interface/js/app/rspamd.js @@ -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 = { + "&": "&", + "<": "<", + ">": ">", + "\"": """, + "'": "'", + "/": "/", + "`": "`", + "=": "=" + }; + 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(",​") + 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; + } + + 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; }); diff --git a/interface/js/app/upload.js b/interface/js/app/upload.js index a21d82484..0a0640c51 100644 --- a/interface/js/app/upload.js +++ b/interface/js/app/upload.js @@ -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); } |