diff options
author | Vsevolod Stakhov <vsevolod@rspamd.com> | 2025-06-30 17:44:55 +0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-06-30 17:44:55 +0600 |
commit | 1bc61e5959fcf1d1cf4b57cf6496630e32a9ec95 (patch) | |
tree | e9dc22f383d730bfcce04c3f6e8e167cb378b7ba /interface | |
parent | 09756fbbc2d5901958b7099420dd648d627b40ee (diff) | |
parent | ce2dba2f58b8dacbf0569d31d40e2c001c1cc0ab (diff) | |
download | rspamd-master.tar.gz rspamd-master.zip |
[WebUI] Add fuzzy flag selectors to Scan/Learn tab
Diffstat (limited to 'interface')
-rw-r--r-- | interface/css/rspamd.css | 9 | ||||
-rw-r--r-- | interface/index.html | 4 | ||||
-rw-r--r-- | interface/js/app/common.js | 72 | ||||
-rw-r--r-- | interface/js/app/rspamd.js | 2 | ||||
-rw-r--r-- | interface/js/app/upload.js | 94 |
5 files changed, 164 insertions, 17 deletions
diff --git a/interface/css/rspamd.css b/interface/css/rspamd.css index a79fed28b..a4335c07b 100644 --- a/interface/css/rspamd.css +++ b/interface/css/rspamd.css @@ -95,6 +95,15 @@ fieldset[disabled] .btn { pointer-events: auto; cursor: not-allowed; } +.card.disabled, +.input-group.disabled { + cursor: not-allowed; + opacity: 0.65; +} +.card.disabled *, +.input-group.disabled * { + pointer-events: none; +} .w-1 { width: 1%; } diff --git a/interface/index.html b/interface/index.html index 61487ce29..30181e788 100644 --- a/interface/index.html +++ b/interface/index.html @@ -453,7 +453,8 @@ </div> <div class="input-group d-inline-flex w-auto my-1"> <label for="fuzzy-flag" class="input-group-text">Flag</label> - <input id="fuzzy-flag" class="form-control" value="1" min="1" type="number"> + <select id="fuzzy-flag-picker" class="form-select"></select> + <input id="fuzzy-flag" class="form-control flex-grow-0" value="1" min="1" type="number"> <button class="btn btn-warning d-flex align-items-center" data-upload="compute-fuzzy"><i class="fas fa-hashtag me-2"></i>Compute fuzzy hashes</button> </div> <div class="float-end my-1"> @@ -495,6 +496,7 @@ <div class="row g-2 align-items-center"> <div class="col-auto d-flex align-items-center me-1"> <label for="fuzzyFlagText" class="me-1">Flag:</label> + <select id="fuzzyFlagText-picker" class="form-select"></select> <input id="fuzzyFlagText" class="form-control" type="number" value="1"/> </div> <div class="col-auto d-flex align-items-center me-2"> diff --git a/interface/js/app/common.js b/interface/js/app/common.js index 44e0dcf77..6f37d739e 100644 --- a/interface/js/app/common.js +++ b/interface/js/app/common.js @@ -57,6 +57,20 @@ define(["jquery", "nprogress"], }, 5000); } + /** + * Perform a request to a single Rspamd neighbour server. + * + * @param {Array.<Object>} neighbours_status + * Array of neighbour status objects. + * @param {number} ind + * Index of this neighbour in the `neighbours_status` array. + * @param {string} req_url + * Relative controller endpoint with optional query string. + * @param {Object} o + * The same `options` object passed into `ui.query`. + * + * @returns {void} + */ function queryServer(neighbours_status, ind, req_url, o) { neighbours_status[ind].checked = false; neighbours_status[ind].data = {}; @@ -152,23 +166,51 @@ define(["jquery", "nprogress"], }; /** - * @param {string} url - A string containing the URL to which the request is sent - * @param {Object} [options] - A set of key/value pairs that configure the Ajax request. All settings are optional. + * Perform an HTTP request to one or all Rspamd neighbours. * - * @param {Function} [options.complete] - A function to be called when the requests to all neighbours complete. - * @param {Object|string|Array} [options.data] - Data to be sent to the server. - * @param {Function} [options.error] - A function to be called if the request fails. - * @param {string} [options.errorMessage] - Text to display in the alert message if the request fails. - * @param {string} [options.errorOnceId] - A prefix of the alert ID to be added to the session storage. If the - * parameter is set, the error for each server will be displayed only once per session. - * @param {Object} [options.headers] - An object of additional header key/value pairs to send along with requests - * using the XMLHttpRequest transport. - * @param {string} [options.method] - The HTTP method to use for the request. - * @param {Object} [options.params] - An object of additional jQuery.ajax() settings key/value pairs. - * @param {string} [options.server] - A server to which send the request. - * @param {Function} [options.success] - A function to be called if the request succeeds. + * @param {string} url + * Relative URL, including with optional query string (e.g. "plugins/selectors/check_selector?selector=from"). + * @param {Object} [options] + * Ajax request configuration options. + * @param {Object|string|Array} [options.data] + * Request body for POST endpoints. + * @param {Object} [options.headers] + * Additional HTTP headers. + * @param {"GET"|"POST"} [options.method] + * HTTP method (defaults to "GET"). + * @param {string} [options.server] + * Name or base-URL of the target server (defaults to the currently selected Rspamd neighbour). + * @param {Object} [options.params] + * Extra jQuery.ajax() settings (e.g. timeout, dataType). + * @param {string} [options.errorMessage] + * Text to show inside a Bootstrap alert on generic errors (e.g. network failure). + * @param {string} [options.errorOnceId] + * Prefix for an alert ID stored in session storage to ensure + * `errorMessage` is shown only once per server each session. + * @param {function(Array.<Object>, Object)} [options.success] + * Called on HTTP success. Receives: + * 1. results: Array of per-server status objects: + * { + * name: string, + * host: string, + * url: string, // full URL base for this neighbour + * checked: boolean, // whether this server was attempted + * status: boolean, // HTTP success (<400) + * data: any, // parsed JSON or raw text + * percentComplete: number + * } + * 2. jqXHR: jQuery XHR object with properties + * { readyState, status, statusText, responseText, responseJSON, … } + * @param {function(Object, Object, string, string)} [options.error] + * Called on HTTP error or network failure. Receives: + * 1. result: a per-server status object (status:false, data:{}). + * 2. jqXHR: jQuery XHR object (responseText, responseJSON, status, statusText). + * 3. textStatus: string describing error type ("error", "timeout", etc.). + * 4. errorThrown: exception message or HTTP statusText. + * @param {function()} [options.complete] + * Called once all servers have been tried; takes no arguments. * - * @returns {undefined} + * @returns {void} */ ui.query = function (url, options) { // Force options to be an object diff --git a/interface/js/app/rspamd.js b/interface/js/app/rspamd.js index da4e9bccf..ceba6864b 100644 --- a/interface/js/app/rspamd.js +++ b/interface/js/app/rspamd.js @@ -176,7 +176,7 @@ define(["jquery", "app/common", "stickytabs", "visibility", require(["app/symbols"], (module) => module.getSymbols()); break; case "#scan_nav": - require(["app/upload"]); + require(["app/upload"], (module) => module.getFuzzyStorages()); break; case "#selectors_nav": require(["app/selectors"], (module) => module.displayUI()); diff --git a/interface/js/app/upload.js b/interface/js/app/upload.js index 0960ebf25..c82bb2306 100644 --- a/interface/js/app/upload.js +++ b/interface/js/app/upload.js @@ -312,5 +312,99 @@ define(["jquery", "app/common", "app/libft"], }; ui.getClassifiers(); + + const fuzzyWidgets = [ + { + picker: "#fuzzy-flag-picker", + input: "#fuzzy-flag", + container: ($picker) => $picker.parent() + }, + { + picker: "#fuzzyFlagText-picker", + input: "#fuzzyFlagText", + container: ($picker) => $picker.closest("div.card") + } + ]; + + function toggleWidgets(showPicker, showInput) { + fuzzyWidgets.forEach(({picker, input}) => { + $(picker)[showPicker ? "show" : "hide"](); + $(input)[showInput ? "show" : "hide"](); + }); + } + + function setWidgetsDisabled(disable) { + fuzzyWidgets.forEach(({picker, container}) => { + container($(picker))[disable ? "addClass" : "removeClass"]("disabled"); + }); + } + + let lastFuzzyStoragesReq = {config_id: null, server: null}; + + ui.getFuzzyStorages = function () { + const server = common.getServer(); + + const servers = JSON.parse(sessionStorage.getItem("Credentials") || "{}"); + const config_id = servers[server]?.data?.config_id; + + if ((config_id && config_id === lastFuzzyStoragesReq.config_id) || + (!config_id && server === lastFuzzyStoragesReq.server)) { + return; + } + lastFuzzyStoragesReq = {config_id: config_id, server: server}; + + fuzzyWidgets.forEach(({picker, container}) => container($(picker)).removeAttr("title")); + + common.query("plugins/fuzzy/storages", { + success: function (data) { + const storages = data[0].data.storages || {}; + const hasWritableStorages = Object.keys(storages).some((name) => !storages[name].read_only); + + toggleWidgets(true, false); + setWidgetsDisabled(!hasWritableStorages); + + fuzzyWidgets.forEach(({picker, input}) => { + const $sel = $(picker); + + $sel.empty(); + + if (hasWritableStorages) { + Object.entries(storages).forEach(([name, info]) => { + if (!info.read_only) { + Object.entries(info.flags).forEach(([symbol, val]) => { + $sel.append($("<option>", {value: val, text: `${name}:${symbol} (${val})`})); + }); + } + }); + $(input).val($sel.val()); + $sel.off("change").on("change", () => $(input).val($sel.val())); + } else { + $sel.append($("<option>", {value: "", text: "No writable storages"})); + } + }); + }, + error: function (_result, _jqXHR, _textStatus, errorThrown) { + if (errorThrown === "fuzzy_check is not enabled") { + toggleWidgets(true, false); + setWidgetsDisabled(true); + + fuzzyWidgets.forEach(({picker, container}) => { + const $picker = $(picker); + $picker + .empty() + .append($("<option>", {value: "", text: "fuzzy_check disabled"})) + .show(); + container($picker) + .attr("title", "fuzzy_check module is not enabled in server configuration."); + }); + } else { + toggleWidgets(false, true); + setWidgetsDisabled(false); + } + }, + server: server + }); + }; + return ui; }); |