From 04618fd51b3fed8c5015c988a49fd728a5585236 Mon Sep 17 00:00:00 2001 From: Alexander Moisseev Date: Mon, 23 Jun 2025 18:14:22 +0300 Subject: [Minor] Improve JSDoc for ui.query() --- interface/js/app/common.js | 72 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 15 deletions(-) (limited to 'interface/js') 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.} 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)} [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 -- cgit v1.2.3 From ce2dba2f58b8dacbf0569d31d40e2c001c1cc0ab Mon Sep 17 00:00:00 2001 From: Alexander Moisseev Date: Wed, 25 Jun 2025 17:20:19 +0300 Subject: [WebUI] Add fuzzy flag selectors to Scan/Learn tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Populate selectors with symbols and flags from writable fuzzy storages. - Cache each server’s config_id to avoid redundant `/plugins/fuzzy/storages` calls. - Dynamically show, hide, and disable controls based on fuzzy_check module availability and storage writability. --- eslint.config.mjs | 3 ++ interface/css/rspamd.css | 9 +++++ interface/index.html | 4 +- interface/js/app/rspamd.js | 2 +- interface/js/app/upload.js | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 2 deletions(-) (limited to 'interface/js') diff --git a/eslint.config.mjs b/eslint.config.mjs index 2f256afcb..bdd6ede48 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,6 +13,9 @@ export default [ ...globals.browser, define: false, }, + parserOptions: { + ecmaVersion: 2020, + }, sourceType: "script", }, plugins: { 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 @@
- + +
@@ -495,6 +496,7 @@
+
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($("