update settings editor, mode and template

* add template for editor container to used to generate a new editor config in the same way of multiples
* render editors looking custom configs in settings (instanciate ace, update attribute, add to template, update value and input logic, ...)
* case no instance on an editor container, create default empty config editor
* handle custom config on simple mode filterSettings
* now remove conflicts settings on filterSettings and merge remaining settings on both main and compare settings
* force simple mode on new service and advanced mode on edit service
This commit is contained in:
Jordan Blasenhauer 2024-04-30 17:36:45 +02:00
parent fe9ceab961
commit 94e234ef58
4 changed files with 233 additions and 33 deletions

View file

@ -129,7 +129,12 @@ class SettingsService {
setMethodUI,
emptyServerName,
);
const modeBtn = document.querySelector(
"button[data-toggle-settings-mode-btn]",
);
const mode = modeBtn.getAttribute("data-toggle-settings-mode-btn");
if (action === "new") {
mode !== "simple" ? modeBtn.click() : null;
document
.querySelector(
`button[data-setting-select-dropdown-btn="security-level"][value="standard"]`,
@ -141,6 +146,7 @@ class SettingsService {
)
.setAttribute("disabled", "true");
} else {
mode !== "advanced" ? modeBtn.click() : null;
document
.querySelector(
`button[data-setting-select-dropdown-btn="security-level"][value="custom"]`,
@ -154,7 +160,6 @@ class SettingsService {
}
}
} catch (err) {
console.log(err);
}
// security level
try {
@ -241,7 +246,8 @@ class SettingsService {
true,
);
}
} catch (err) {}
} catch (err) {
}
});
}
}
@ -317,7 +323,6 @@ class ServiceModal {
this.setFormModal(e.target);
}
} catch (err) {
console.log(err);
}
});
}

View file

@ -1032,6 +1032,7 @@ class Settings {
inpName === "is_draft" ||
inpName === "operation" ||
inpName === "settings-filter" ||
inpName === "CONFIG_NAME" ||
inp.hasAttribute("data-combobox")
)
return true;
@ -1732,32 +1733,168 @@ class SettingsEditor extends SettingsMultiple {
}
initEditors() {
window.addEventListener("load", () => {
this.instanciateEditors();
});
this.darkMode.addEventListener("click", (e) => {
this.isDarkMode = e.target.checked;
this.updateEditorMode();
});
}
instanciateEditors() {
const editors = this.container.querySelectorAll("[data-editor]");
editors.forEach((editorEl) => {
const editor = ace.edit(editorEl.getAttribute("id"));
// Handle
if (this.isDarkMode) {
editor.setTheme("ace/theme/dracula");
} else {
editor.setTheme("ace/theme/dawn");
setupEditorsInstance() {
this.editorEls.forEach((editor) => {
const editorEl = editor.container;
// we want to link editor to inp when sending form
const linkInp = editorEl
.closest("[data-editor-container]")
.querySelector(`textarea[data-editor-input]`);
// format name to get format TYPE_CONFIG_NAME
linkInp.addEventListener("change", () => {
const filename = linkInp?.getAttribute("data-filename")
? linkInp?.getAttribute("data-filename")
: linkInp?.getAttribute("data-default-filename");
const type = linkInp?.getAttribute("data-config-type");
const action = linkInp?.getAttribute("data-action");
linkInp.setAttribute("name", `${type}_${filename}_${action}`);
});
editor.on("change", () => {
linkInp.value = editor.getValue();
});
// we can link inp to input file name to update it if exists
const inpFileName = editorEl
.closest("[data-editor-container]")
.querySelector(`input[data-editor-filename]`);
if (inpFileName) {
inpFileName.addEventListener("input", () => {
linkInp.setAttribute("data-filename", inpFileName.value);
// dispatch event to inp to ensure it is updated
const event = new Event("change");
linkInp.dispatchEvent(event);
});
inpFileName.addEventListener("change", () => {
linkInp.setAttribute("data-filename", inpFileName.value);
// dispatch event to inp to ensure it is updated
const event = new Event("change");
linkInp.dispatchEvent(event);
});
}
//editor options
editor.setShowPrintMargin(false);
this.editorEls.push(editor);
});
}
setEditorSettings() {
this.resetEditorsInstAndDOM();
this.addDefaultEditorIfNone();
this.setupEditorsInstance();
this.updateEditorMode();
}
addDefaultEditorIfNone() {
// get containers with _SCHEMA
const editorContainers = this.container.querySelectorAll(
"[data-editor-container$='_SCHEMA']",
);
editorContainers.forEach((editorContainer) => {
// Check if others editor exists with same base name
const editorName = editorContainer
.getAttribute("data-editor-container")
.replace("_SCHEMA", "");
const otherEditors = this.container.querySelectorAll(
`[data-editor-container*='${editorName}']`,
);
if (otherEditors.length > 1) return;
// Add default editor
const defaultType = editorContainer.getAttribute("data-default-type");
const defaultName = editorContainer.getAttribute("data-default-name");
this.addOneEditor(editorContainer, defaultType, defaultName, 1, "");
});
}
resetEditorsInstAndDOM() {
// reset previous editors
this.editorEls.forEach((editor) => {
const editorContainer = editor.container.closest(
"[data-editor-container]",
);
editorContainer.remove();
editor.destroy();
});
this.editorEls = [];
// get only container ending with _SCHEMA
const editorContainers = this.container.querySelectorAll(
"[data-editor-container$='_SCHEMA']",
);
const configsSettings = this.getEditorSettings();
// Create instances on the right containers
editorContainers.forEach((editorContainer) => {
const contName = editorContainer
.getAttribute("data-editor-container")
.replace("_SCHEMA", "");
// Loop on each custom config settings that match same prefix as key
// And create instance
for (const [key, data] of Object.entries(configsSettings)) {
if (!key.startsWith(contName)) continue;
const editorName = data["name"];
const editorType = data["type"];
const editorValue = data["value"];
const [num, isNum, name] = this.getSuffixData(key);
this.addOneEditor(
editorContainer,
editorType,
editorName,
num,
editorValue,
);
}
});
}
addOneEditor(container, type, name, num, value) {
const contName = container
.getAttribute("data-editor-container")
.replace("_SCHEMA", "");
const containerClone = container.cloneNode(true);
// update attributs
containerClone.setAttribute("data-editor-container", `${contName}_${num}`);
const editor = containerClone.querySelector(`[data-editor]`);
if (editor) {
editor.setAttribute("id", `${contName}_${num}`);
editor.setAttribute("name", `${contName}_${num}`);
}
const filenameInp = containerClone.querySelector(
`input[data-editor-filename]`,
);
if (filenameInp) filenameInp.value = name;
const hiddenInp = containerClone.querySelector(
`textarea[data-editor-input]`,
);
if (hiddenInp) {
hiddenInp.setAttribute("data-config-type", type);
hiddenInp.setAttribute("data-filename", name);
hiddenInp.setAttribute("data-action", this.currAction);
hiddenInp.setAttribute("name", `${type}_${name}_${this.currAction}`);
}
// append to DOM and show as sibling of the original container
container.insertAdjacentElement("afterend", containerClone);
containerClone.classList.remove("hidden");
// instantiate editor
const editorInst = ace.edit(editor);
editorInst.setValue(value);
this.editorEls.push(editorInst);
}
getEditorSettings() {
const settings = JSON.parse(JSON.stringify(this.currSettings));
const configsSettings = {};
for (const [key, data] of Object.entries(settings)) {
if (key.startsWith("CUSTOM_CONFIG")) {
configsSettings[key] = data;
}
}
return configsSettings;
}
updateEditorMode() {
this.editorEls.forEach((editor) => {
if (this.isDarkMode) {
@ -1927,6 +2064,7 @@ class SettingsSimple extends SettingsEditor {
compareSettings && Object.keys(compareSettings).length > 0
? this.filterSettings(mainSettings, compareSettings)
: mainSettings;
this.updateData(
action,
oldServName,
@ -1937,6 +2075,7 @@ class SettingsSimple extends SettingsEditor {
emptyServerName,
);
this.setSettingsSimple();
this.setEditorSettings();
this.resetServerName();
if (resetSteps) this.resetSimpleMode();
this.checkVisibleInpsValidity();
@ -1944,12 +2083,35 @@ class SettingsSimple extends SettingsEditor {
filterSettings(mainSettings, compareSettings) {
const mergeSettings = {};
// handle custom configs, we only keep security level config on new, else we get only service configs
const configsToGetFrom =
this.currAction === "new" ? compareSettings : mainSettings;
const customConfSettings = []; // Allow to delete custom configs
for (const [key, value] of Object.entries(configsToGetFrom)) {
if (key.startsWith("CUSTOM_CONFIG")) {
mergeSettings[key] = value;
customConfSettings.push(key);
}
}
// Delete merged custom configs
for (let i = 0; i < customConfSettings.length; i++) {
try {
delete mainSettings[customConfSettings[i]];
} catch (e) {}
try {
delete compareSettings[customConfSettings[i]];
} catch (e) {}
}
// get the highest suffix number in mainSettings
let highestMainSuffix = 0;
let highestCompareSuffix = 0;
// This will allow
const settingsConflicts = [];
for (const [key, value] of Object.entries(mainSettings)) {
const [mainSuffix, mainIsSuffixe, mainName] = this.getSuffixData(key);
// Case same key (same setting) and not a multiple
// Keep the one with a method != than ui or default if exists
// Else keep the one from compareSettings that is the securityLevel
@ -1957,17 +2119,18 @@ class SettingsSimple extends SettingsEditor {
const method = mainSettings[key]["method"];
if (method !== "ui" && method !== "default") {
mergeSettings[key] = value;
continue;
} else {
highestMainSuffix = mergeSettings[key] = compareSettings[key];
continue;
}
settingsConflicts.push(key);
continue;
}
// Need to check if is a multiple from a list because we can have custom configs with suffixe too
if (this.multSettingsName.includes(mainName)) {
highestMainSuffix = mainIsSuffixe
? Math.max(highestMainSuffix, suffixeNum)
: highestMainSuffix;
settingsConflicts.push(key);
}
const [compareSuffix, compareIsSuffixe, compareName] =
this.getSuffixData(key);
@ -1977,6 +2140,7 @@ class SettingsSimple extends SettingsEditor {
highestCompareSuffix = compareIsSuffixe
? Math.max(highestCompareSuffix, suffixeNum)
: highestCompareSuffix;
settingsConflicts.push(key);
}
}
@ -2013,7 +2177,24 @@ class SettingsSimple extends SettingsEditor {
mergeSettings[key] = value;
}
}
return mergeSettings;
// Delete conflicts settings in order to merge the rest
for (let i = 0; i < settingsConflicts.length; i++) {
try {
delete mainSettings[settingsConflicts[i]];
} catch (e) {}
try {
delete compareSettings[settingsConflicts[i]];
} catch (e) {}
}
// Merge the rest of the settings
const mergeAllSettings = {
...mergeSettings,
...mainSettings,
...compareSettings,
};
return mergeAllSettings;
}
setSettingsSimple() {
@ -2218,7 +2399,7 @@ class SettingsSwitch {
this.prefix = prefix;
this.modes = modes;
this.switchModeBtn = switchBtn;
// dict wth mode as key and form element as value
// dict with mode as key and form element as value
this.container = container;
this.init();
}

View file

@ -27,11 +27,15 @@
},
"CUSTOM_CONFIG_MODSEC_1" : {
"value": "test",
"method": "default"
"method": "default",
"type" : "modsec",
"name" : "modsec 1"
},
"CUSTOM_CONFIG_MODSEC_2" : {
"value": "test",
"method": "default"
"value": "test 2",
"method": "default",
"type" : "modsec",
"name" : "modsec 2"
},
}%}
@ -70,8 +74,7 @@
{"plugin_id" : "antibot", "setting_id": "USE_ANTIBOT", "title" : "Define the type of your antibot", "subtitle" : "Javascript, Captcha or Cookie don't need additionnal settings to be fill. Recaptcha, Hcaptcha and Turnstile need secret key and site key delivered from providers." },
],
"configs": [
{"id" : "CUSTOM_CONFIG_MODSEC_1", "name": "my_config_1", "type": "modsec", "subtitle" : "This will determine the modsecurity rules to apply to this service."},
{"id" : "CUSTOM_CONFIG_MODSEC_2", "name": "my_config_2", "type": "server-http", "subtitle" : "This will determine the server http..."}
{"id" : "CUSTOM_CONFIG_MODSEC", "type" : "modsec", "name" : "modsecrules", "subtitle" : "This will determine the modsecurity rules to apply to this service."},
]
},
]

View file

@ -77,15 +77,26 @@
<div class="col-span-12" data-setting-header>
<div class="flex flex-col justify-start items-start">
<h5 class="sm:pl-3 sm:pr-2 mt-2 transition duration-300 ease-in-out font-bold text-base uppercase dark:text-white/90 mb-0">
Custom config for {{ config['type'] }}
Custom config {{ config['type'] }} for your service.
</h5>
<p class="max-w-[550px] sm:pl-3 sm:pr-2 text-sm dark:text-gray-300 mb-0">{{ config['subtitle'] }}</p>
</div>
</div>
<div data-editor-container class="w-full col-span-12 px-4">
<textarea data-editor-input class="hidden" name="{{config['type']}}_SERVER_NAME_{{config['name']}}"></textarea>
<div data-editor-container="{{config['id']}}_SCHEMA" data-default-type="{{config['type']}}" data-default-name="{{config['name']}}" class="hidden w-full col-span-12 px-4">
<div class="mb-2 flex flex-wrap justify-start items-center" >
<input disabled type="text"
name="CONFIG_NAME"
data-editor-filename
class="dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 sm:ml-1 max-w-40 focus:valid:border-green-500 focus:file:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 font-normal text-gray-700 transition-all placeholder:text-gray-500 disabled:bg-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 dark:disabled:text-gray-300 disabled:text-gray-700"
placeholder="{{config['name']}}"
required />
<p class="ml-1 mb-0 dark:text-white/80 text-gray-700/80 text-sm">
.conf
</p>
</div>
<textarea data-editor-input class="hidden"></textarea>
<!-- editor-->
<div data-editor id="{{config['id']}}" class="text-base w-full h-50 overflow-hidden overflow-y-auto my-2 border border-gray-300 dark:border-slate-800">
<div data-editor class="text-base w-full h-50 overflow-hidden overflow-y-auto my-2 border border-gray-300 dark:border-slate-800">
</div>
<!-- editor-->
</div>