commit 01a0df9: [Minor] Move common stuff to separate files
moisseev
moiseev at mezonplus.ru
Mon Jul 29 17:50:52 UTC 2024
Author: moisseev
Date: 2023-12-27 15:22:23 +0300
URL: https://github.com/rspamd/rspamd/commit/01a0df9f25cee9e56294aea179e6a4e5f1b9c09a
[Minor] Move common stuff to separate files
---
interface/js/app/common.js | 233 ++++++++++++++++
interface/js/app/config.js | 34 +--
interface/js/app/graph.js | 24 +-
interface/js/app/history.js | 52 ++--
interface/js/app/libft.js | 380 +++++++++++++++++++++++++
interface/js/app/rspamd.js | 630 ++----------------------------------------
interface/js/app/selectors.js | 20 +-
interface/js/app/stats.js | 12 +-
interface/js/app/symbols.js | 20 +-
interface/js/app/upload.js | 58 ++--
10 files changed, 741 insertions(+), 722 deletions(-)
diff --git a/interface/js/app/common.js b/interface/js/app/common.js
new file mode 100644
index 000000000..ea6102f60
--- /dev/null
+++ b/interface/js/app/common.js
@@ -0,0 +1,233 @@
+/* global jQuery */
+
+define(["jquery", "nprogress"],
+ ($, NProgress) => {
+ "use strict";
+ const ui = {
+ chartLegend: [
+ {label: "reject", color: "#FF0000"},
+ {label: "soft reject", color: "#BF8040"},
+ {label: "rewrite subject", color: "#FF6600"},
+ {label: "add header", color: "#FFAD00"},
+ {label: "greylist", color: "#436EEE"},
+ {label: "no action", color: "#66CC00"}
+ ],
+ locale: (localStorage.getItem("selected_locale") === "custom") ? localStorage.getItem("custom_locale") : null,
+ neighbours: [],
+ page_size: {
+ scan: 25,
+ errors: 25,
+ history: 25
+ },
+ symbols: {
+ scan: [],
+ history: []
+ },
+ tables: {}
+ };
+
+
+ NProgress.configure({
+ minimum: 0.01,
+ showSpinner: false,
+ });
+
+ function getPassword() {
+ return sessionStorage.getItem("Password");
+ }
+
+ function alertMessage(alertClass, alertText) {
+ const a = $("<div class=\"alert " + alertClass + " alert-dismissible fade in show\">" +
+ "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" title=\"Dismiss\"></button>" +
+ "<strong>" + alertText + "</strong>");
+ $(".notification-area").append(a);
+
+ setTimeout(() => {
+ $(a).fadeTo(500, 0).slideUp(500, function () {
+ $(this).alert("close");
+ });
+ }, 5000);
+ }
+
+ function queryServer(neighbours_status, ind, req_url, o) {
+ neighbours_status[ind].checked = false;
+ neighbours_status[ind].data = {};
+ neighbours_status[ind].status = false;
+ const req_params = {
+ jsonp: false,
+ data: o.data,
+ headers: $.extend({Password: getPassword()}, o.headers),
+ url: neighbours_status[ind].url + req_url,
+ xhr: function () {
+ const xhr = $.ajaxSettings.xhr();
+ // Download progress
+ if (req_url !== "neighbours") {
+ xhr.addEventListener("progress", (e) => {
+ if (e.lengthComputable) {
+ neighbours_status[ind].percentComplete = e.loaded / e.total;
+ const percentComplete = neighbours_status
+ .reduce((prev, curr) => (curr.percentComplete ? curr.percentComplete + prev : prev), 0);
+ NProgress.set(percentComplete / neighbours_status.length);
+ }
+ }, false);
+ }
+ return xhr;
+ },
+ success: function (json) {
+ neighbours_status[ind].checked = true;
+ neighbours_status[ind].status = true;
+ neighbours_status[ind].data = json;
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ neighbours_status[ind].checked = true;
+ function errorMessage() {
+ alertMessage("alert-error", neighbours_status[ind].name + " > " +
+ (o.errorMessage ? o.errorMessage : "Request failed") +
+ (errorThrown ? ": " + errorThrown : ""));
+ }
+ if (o.error) {
+ o.error(neighbours_status[ind],
+ jqXHR, textStatus, errorThrown);
+ } else if (o.errorOnceId) {
+ const alert_status = o.errorOnceId + neighbours_status[ind].name;
+ if (!(alert_status in sessionStorage)) {
+ sessionStorage.setItem(alert_status, true);
+ errorMessage();
+ }
+ } else {
+ errorMessage();
+ }
+ },
+ complete: function (jqXHR) {
+ if (neighbours_status.every((elt) => elt.checked)) {
+ if (neighbours_status.some((elt) => elt.status)) {
+ if (o.success) {
+ o.success(neighbours_status, jqXHR);
+ } else {
+ alertMessage("alert-success", "Request completed");
+ }
+ } else {
+ alertMessage("alert-error", "Request failed");
+ }
+ if (o.complete) o.complete();
+ NProgress.done();
+ }
+ },
+ statusCode: o.statusCode
+ };
+ if (o.method) {
+ req_params.method = o.method;
+ }
+ if (o.params) {
+ $.each(o.params, (k, v) => {
+ req_params[k] = v;
+ });
+ }
+ $.ajax(req_params);
+ }
+
+
+ // Public functions
+
+ ui.alertMessage = alertMessage;
+ ui.getPassword = getPassword;
+
+ // Get selectors' current state
+ ui.getSelector = function (id) {
+ const e = document.getElementById(id);
+ return e.options[e.selectedIndex].value;
+ };
+
+ /**
+ * @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.
+ *
+ * @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.
+ *
+ * @returns {undefined}
+ */
+ ui.query = function (url, options) {
+ // Force options to be an object
+ const o = options || {};
+ Object.keys(o).forEach((option) => {
+ if (["complete", "data", "error", "errorMessage", "errorOnceId", "headers", "method", "params", "server",
+ "statusCode", "success"]
+ .indexOf(option) < 0) {
+ throw new Error("Unknown option: " + option);
+ }
+ });
+
+ let neighbours_status = [{
+ name: "local",
+ host: "local",
+ url: "",
+ }];
+ o.server = o.server || ui.getSelector("selSrv");
+ if (o.server === "All SERVERS") {
+ queryServer(neighbours_status, 0, "neighbours", {
+ success: function (json) {
+ const [{data}] = json;
+ if (jQuery.isEmptyObject(data)) {
+ ui.neighbours = {
+ local: {
+ host: window.location.host,
+ url: window.location.origin + window.location.pathname
+ }
+ };
+ } else {
+ ui.neighbours = data;
+ }
+ neighbours_status = [];
+ $.each(ui.neighbours, (ind) => {
+ neighbours_status.push({
+ name: ind,
+ host: ui.neighbours[ind].host,
+ url: ui.neighbours[ind].url,
+ });
+ });
+ $.each(neighbours_status, (ind) => {
+ queryServer(neighbours_status, ind, url, o);
+ });
+ },
+ errorMessage: "Cannot receive neighbours data"
+ });
+ } else {
+ if (o.server !== "local") {
+ neighbours_status = [{
+ name: o.server,
+ host: ui.neighbours[o.server].host,
+ url: ui.neighbours[o.server].url,
+ }];
+ }
+ queryServer(neighbours_status, 0, url, o);
+ }
+ };
+
+ ui.escapeHTML = function (string) {
+ const htmlEscaper = /[&<>"'/`=]/g;
+ const htmlEscapes = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ "\"": """,
+ "'": "'",
+ "/": "/",
+ "`": "`",
+ "=": "="
+ };
+ return String(string).replace(htmlEscaper, (match) => htmlEscapes[match]);
+ };
+
+ return ui;
+ });
diff --git a/interface/js/app/config.js b/interface/js/app/config.js
index 1aaf71289..6be107555 100644
--- a/interface/js/app/config.js
+++ b/interface/js/app/config.js
@@ -24,13 +24,13 @@
/* global require */
-define(["jquery", "app/rspamd"],
- ($, rspamd) => {
+define(["jquery", "app/common"],
+ ($, common) => {
"use strict";
const ui = {};
ui.getActions = function getActions(checked_server) {
- rspamd.query("actions", {
+ common.query("actions", {
success: function (data) {
$("#actionsFormField").empty();
const items = [];
@@ -88,15 +88,15 @@ define(["jquery", "app/rspamd"],
// String to array for comparison
const eltsArray = JSON.parse(elts);
if (eltsArray[0] < 0) {
- rspamd.alertMessage("alert-modal alert-error", "Spam can not be negative");
+ common.alertMessage("alert-modal alert-error", "Spam can not be negative");
} else if (eltsArray[1] < 0) {
- rspamd.alertMessage("alert-modal alert-error", "Rewrite subject can not be negative");
+ common.alertMessage("alert-modal alert-error", "Rewrite subject can not be negative");
} else if (eltsArray[2] < 0) {
- rspamd.alertMessage("alert-modal alert-error", "Probable spam can not be negative");
+ common.alertMessage("alert-modal alert-error", "Probable spam can not be negative");
} else if (eltsArray[3] < 0) {
- rspamd.alertMessage("alert-modal alert-error", "Greylist can not be negative");
+ common.alertMessage("alert-modal alert-error", "Greylist can not be negative");
} else if (descending(eltsArray)) {
- rspamd.query("saveactions", {
+ common.query("saveactions", {
method: "POST",
params: {
data: elts,
@@ -105,14 +105,14 @@ define(["jquery", "app/rspamd"],
server: server
});
} else {
- rspamd.alertMessage("alert-modal alert-error", "Incorrect order of actions thresholds");
+ common.alertMessage("alert-modal alert-error", "Incorrect order of actions thresholds");
}
};
ui.getMaps = function (checked_server) {
const $listmaps = $("#listMaps");
$listmaps.closest(".card").hide();
- rspamd.query("maps", {
+ common.query("maps", {
success: function (json) {
const [{data}] = json;
$listmaps.empty();
@@ -121,7 +121,7 @@ define(["jquery", "app/rspamd"],
$.each(data, (i, item) => {
let $td = '<td><span class="badge text-bg-secondary">Read</span></td>';
- if (!(item.editable === false || rspamd.read_only)) {
+ if (!(item.editable === false || common.read_only)) {
$td = $($td).append(' <span class="badge text-bg-success">Write</span>');
}
const $tr = $("<tr>").append($td);
@@ -158,9 +158,9 @@ define(["jquery", "app/rspamd"],
// Modal form for maps
$(document).on("click", "[data-bs-toggle=\"modal\"]", function () {
- const checked_server = rspamd.getSelector("selSrv");
+ const checked_server = common.getSelector("selSrv");
const item = $(this).data("item");
- rspamd.query("getmap", {
+ common.query("getmap", {
headers: {
Map: item.map
},
@@ -180,11 +180,11 @@ define(["jquery", "app/rspamd"],
jar.updateCode(data[0].data);
});
} else {
- document.querySelector("#editor").innerHTML = rspamd.escapeHTML(data[0].data);
+ document.querySelector("#editor").innerHTML = common.escapeHTML(data[0].data);
}
let icon = "fa-edit";
- if (item.editable === false || rspamd.read_only) {
+ if (item.editable === false || common.read_only) {
$("#editor").attr(editor[mode].readonly_attr);
icon = "fa-eye";
$("#modalSaveGroup").hide();
@@ -218,9 +218,9 @@ define(["jquery", "app/rspamd"],
});
function saveMap(server) {
- rspamd.query("savemap", {
+ common.query("savemap", {
success: function () {
- rspamd.alertMessage("alert-success", "Map data successfully saved");
+ common.alertMessage("alert-success", "Map data successfully saved");
$("#modalDialog").modal("hide");
},
errorMessage: "Save map error",
diff --git a/interface/js/app/graph.js b/interface/js/app/graph.js
index 2fc00a457..71306f457 100644
--- a/interface/js/app/graph.js
+++ b/interface/js/app/graph.js
@@ -25,8 +25,8 @@
/* global FooTable */
-define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"],
- ($, rspamd, D3Evolution, D3Pie, d3) => {
+define(["jquery", "app/common", "d3evolution", "d3pie", "d3", "footable"],
+ ($, common, D3Evolution, D3Pie, d3) => {
"use strict";
const rrd_pie_config = {
@@ -68,16 +68,16 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"],
legend: {
space: 140,
- entries: rspamd.chartLegend
+ entries: common.chartLegend
}
};
function initGraph() {
const graph = new D3Evolution("graph", $.extend({}, graph_options, {
- yScale: rspamd.getSelector("selYScale"),
- type: rspamd.getSelector("selType"),
- interpolate: rspamd.getSelector("selInterpolate"),
- convert: rspamd.getSelector("selConvert"),
+ yScale: common.getSelector("selYScale"),
+ type: common.getSelector("selType"),
+ interpolate: common.getSelector("selInterpolate"),
+ convert: common.getSelector("selConvert"),
}));
$("#selYScale").change(function () {
graph.yScale(this.value);
@@ -127,7 +127,7 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"],
}
function initSummaryTable(rows, unit) {
- rspamd.tables.rrd_summary = FooTable.init("#rrd-table", {
+ common.tables.rrd_summary = FooTable.init("#rrd-table", {
sorting: {
enabled: true
},
@@ -151,8 +151,8 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"],
}
function drawRrdTable(rows, unit) {
- if (Object.prototype.hasOwnProperty.call(rspamd.tables, "rrd_summary")) {
- $.each(rspamd.tables.rrd_summary.rows.all, (i, row) => {
+ if (Object.prototype.hasOwnProperty.call(common.tables, "rrd_summary")) {
+ $.each(common.tables.rrd_summary.rows.all, (i, row) => {
row.val(rows[i], false, true);
});
} else {
@@ -199,7 +199,7 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"],
}
- rspamd.query("graph", {
+ common.query("graph", {
success: function (req_data) {
let data = null;
const neighbours_data = req_data
@@ -214,7 +214,7 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"],
if ((curr[0][0].x !== res[0][0].x) ||
(curr[0][curr[0].length - 1].x !== res[0][res[0].length - 1].x)) {
time_match = false;
- rspamd.alertMessage("alert-error",
+ common.alertMessage("alert-error",
"Neighbours time extents do not match. Check if time is synchronized on all servers.");
arr.splice(1); // Break out of .reduce() by mutating the source array
}
diff --git a/interface/js/app/history.js b/interface/js/app/history.js
index a4d027da0..58d835fba 100644
--- a/interface/js/app/history.js
+++ b/interface/js/app/history.js
@@ -24,8 +24,8 @@
/* global FooTable */
-define(["jquery", "app/rspamd", "d3", "footable"],
- ($, rspamd, d3) => {
+define(["jquery", "app/common", "app/libft", "d3", "footable"],
+ ($, common, libft, d3) => {
"use strict";
const ui = {};
let prevVersion = null;
@@ -38,15 +38,15 @@ define(["jquery", "app/rspamd", "d3", "footable"],
$("#selSymOrder_history, label[for='selSymOrder_history']").hide();
$.each(data, (i, item) => {
- item.time = rspamd.unix_time_format(item.unix_time);
- rspamd.preprocess_item(item);
+ item.time = libft.unix_time_format(item.unix_time);
+ libft.preprocess_item(item);
item.symbols = Object.keys(item.symbols)
.map((key) => item.symbols[key])
.sort(compare)
.map((e) => e.name)
.join(", ");
item.time = {
- value: rspamd.unix_time_format(item.unix_time),
+ value: libft.unix_time_format(item.unix_time),
options: {
sortValue: item.unix_time
}
@@ -234,7 +234,7 @@ define(["jquery", "app/rspamd", "d3", "footable"],
function process_history_data(data) {
const process_functions = {
- 2: rspamd.process_history_v2,
+ 2: libft.process_history_v2,
legacy: process_history_legacy
};
let pf = process_functions.legacy;
@@ -263,12 +263,12 @@ define(["jquery", "app/rspamd", "d3", "footable"],
}
ui.getHistory = function () {
- rspamd.query("history", {
+ common.query("history", {
success: function (req_data) {
function differentVersions(neighbours_data) {
const dv = neighbours_data.some((e) => e.version !== neighbours_data[0].version);
if (dv) {
- rspamd.alertMessage("alert-error",
+ common.alertMessage("alert-error",
"Neighbours history backend versions do not match. Cannot display history.");
return true;
}
@@ -293,21 +293,21 @@ define(["jquery", "app/rspamd", "d3", "footable"],
}
const o = process_history_data(data);
const {items} = o;
- rspamd.symbols.history = o.symbols;
+ common.symbols.history = o.symbols;
- if (Object.prototype.hasOwnProperty.call(rspamd.tables, "history") &&
+ if (Object.prototype.hasOwnProperty.call(common.tables, "history") &&
version === prevVersion) {
- rspamd.tables.history.rows.load(items);
+ common.tables.history.rows.load(items);
} else {
- rspamd.destroyTable("history");
+ libft.destroyTable("history");
// Is there a way to get an event when the table is destroyed?
setTimeout(() => {
- rspamd.initHistoryTable(data, items, "history", get_history_columns(data), false);
+ libft.initHistoryTable(data, items, "history", get_history_columns(data), false);
}, 200);
}
prevVersion = version;
} else {
- rspamd.destroyTable("history");
+ libft.destroyTable("history");
}
},
complete: function () { $("#refresh").removeAttr("disabled").removeClass("disabled"); },
@@ -316,7 +316,7 @@ define(["jquery", "app/rspamd", "d3", "footable"],
};
function initErrorsTable(rows) {
- rspamd.tables.errors = FooTable.init("#errorsLog", {
+ common.tables.errors = FooTable.init("#errorsLog", {
columns: [
{sorted: true,
direction: "DESC",
@@ -340,7 +340,7 @@ define(["jquery", "app/rspamd", "d3", "footable"],
paging: {
enabled: true,
limit: 5,
- size: rspamd.page_size.errors
+ size: common.page_size.errors
},
filtering: {
enabled: true,
@@ -354,9 +354,9 @@ define(["jquery", "app/rspamd", "d3", "footable"],
}
ui.getErrors = function () {
- if (rspamd.read_only) return;
+ if (common.read_only) return;
- rspamd.query("errors", {
+ common.query("errors", {
success: function (data) {
const neighbours_data = data
.filter((d) => d.status) // filter out unavailable neighbours
@@ -364,14 +364,14 @@ define(["jquery", "app/rspamd", "d3", "footable"],
const rows = [].concat.apply([], neighbours_data);
$.each(rows, (i, item) => {
item.ts = {
- value: rspamd.unix_time_format(item.ts),
+ value: libft.unix_time_format(item.ts),
options: {
sortValue: item.ts
}
};
});
- if (Object.prototype.hasOwnProperty.call(rspamd.tables, "errors")) {
- rspamd.tables.errors.rows.load(rows);
+ if (Object.prototype.hasOwnProperty.call(common.tables, "errors")) {
+ common.tables.errors.rows.load(rows);
} else {
initErrorsTable(rows);
}
@@ -386,8 +386,8 @@ define(["jquery", "app/rspamd", "d3", "footable"],
};
- rspamd.set_page_size("history", $("#history_page_size").val());
- rspamd.bindHistoryTableEventHandlers("history", 8);
+ libft.set_page_size("history", $("#history_page_size").val());
+ libft.bindHistoryTableEventHandlers("history", 8);
$("#updateHistory").off("click");
$("#updateHistory").on("click", (e) => {
@@ -402,10 +402,10 @@ define(["jquery", "app/rspamd", "d3", "footable"],
if (!confirm("Are you sure you want to reset history log?")) { // eslint-disable-line no-alert
return;
}
- rspamd.destroyTable("history");
- rspamd.destroyTable("errors");
+ libft.destroyTable("history");
+ libft.destroyTable("errors");
- rspamd.query("historyreset", {
+ common.query("historyreset", {
success: function () {
ui.getHistory();
ui.getErrors();
diff --git a/interface/js/app/libft.js b/interface/js/app/libft.js
new file mode 100644
index 000000000..58262d22f
--- /dev/null
+++ b/interface/js/app/libft.js
@@ -0,0 +1,380 @@
+/* global FooTable */
+
+define(["jquery", "app/common", "footable"],
+ ($, common) => {
+ "use strict";
+ const ui = {};
+
+ let pageSizeTimerId = null;
+ let pageSizeInvocationCounter = 0;
+
+ function get_compare_function(table) {
+ const 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[common.getSelector("selSymOrder_" + table)];
+ }
+
+ function sort_symbols(o, compare_function) {
+ return Object.keys(o)
+ .map((key) => o[key])
+ .sort(compare_function)
+ .map((e) => e.str)
+ .join("<br>\n");
+ }
+
+
+ // Public functions
+
+ ui.set_page_size = function (table, page_size, changeTablePageSize) {
+ const n = parseInt(page_size, 10); // HTML Input elements return string representing a number
+ if (n > 0) {
+ common.page_size[table] = n;
+
+ if (changeTablePageSize &&
+ $("#historyTable_" + table + " tbody").is(":parent")) { // Table is not empty
+ clearTimeout(pageSizeTimerId);
+ const t = FooTable.get("#historyTable_" + table);
+ if (t) {
+ pageSizeInvocationCounter = 0;
+ // Wait for input finish
+ pageSizeTimerId = setTimeout(() => t.pageSize(n), 1000);
+ } else if (++pageSizeInvocationCounter < 10) {
+ // Wait for FooTable instance ready
+ pageSizeTimerId = setTimeout(() => ui.set_page_size(table, n, true), 1000);
+ }
+ }
+ }
+ };
+
+ ui.bindHistoryTableEventHandlers = function (table, symbolsCol) {
+ function change_symbols_order(order) {
+ $(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active");
+ const compare_function = get_compare_function(table);
+ $.each(common.tables[table].rows.all, (i, row) => {
+ const cell_val = sort_symbols(common.symbols[table][i], compare_function);
+ row.cells[symbolsCol].val(cell_val, false, true);
+ });
+ }
+
+ $("#selSymOrder_" + table).unbind().change(function () {
+ const order = this.value;
+ change_symbols_order(order);
+ });
+ $("#" + table + "_page_size").change((e) => ui.set_page_size(table, e.target.value, true));
+ $(document).on("click", ".btn-sym-order-" + table + " input", function () {
+ const order = this.value;
+ $("#selSymOrder_" + table).val(order);
+ change_symbols_order(order);
+ });
+ };
+
+ ui.destroyTable = function (table) {
+ if (common.tables[table]) {
+ common.tables[table].destroy();
+ delete common.tables[table];
+ }
+ };
+
+ ui.initHistoryTable = function (data, items, table, columns, expandFirst) {
+ /* eslint-disable no-underscore-dangle */
+ FooTable.Cell.extend("collapse", function () {
+ // call the original method
+ this._super();
+ // Copy cell classes to detail row tr element
+ this._setClasses(this.$detail);
+ });
+ /* eslint-enable no-underscore-dangle */
*** OUTPUT TRUNCATED, 1399 LINES SKIPPED ***
More information about the Commits
mailing list