Enhance QOL in web UI in general + made advancements in easy mode for services

This commit is contained in:
Théophile Diot 2024-09-14 18:02:18 +02:00
parent 398a33ccfe
commit f73632f8c7
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
18 changed files with 821 additions and 257 deletions

View file

@ -58,23 +58,18 @@ class Config:
def get_plugins_settings(self) -> dict:
return {
**{k: v for x in self.get_plugins() for k, v in x["settings"].items()},
**{k: v for x in self.get_plugins().values() for k, v in x["settings"].items()},
**self.__settings,
}
def get_plugins(self, *, _type: Literal["all", "external", "ui", "pro"] = "all", with_data: bool = False) -> List[dict]:
plugins = self.__db.get_plugins(_type=_type, with_data=with_data)
plugins.sort(key=itemgetter("name"))
def get_plugins(self, *, _type: Literal["all", "external", "ui", "pro"] = "all", with_data: bool = False) -> dict:
db_plugins = self.__db.get_plugins(_type=_type, with_data=with_data)
db_plugins.sort(key=itemgetter("name"))
general_plugin = None
for plugin in plugins.copy():
if plugin["id"] == "general":
general_plugin = plugin
plugins.remove(plugin)
break
plugins = {"general": {}}
if general_plugin:
plugins.insert(0, general_plugin)
for plugin in db_plugins.copy():
plugins[plugin.pop("id")] = plugin
return plugins

View file

@ -302,7 +302,7 @@ def plugins_page():
def update_plugins(threaded: bool = False):
wait_applying()
plugins = BW_CONFIG.get_plugins(_type="ui", with_data=True)
plugins = BW_CONFIG.get_plugins(_type="ui", with_data=True) # TODO: remember that this returns a dict now
for plugin in deepcopy(plugins):
if plugin["id"] in new_plugins_ids:
flash(f"Plugin {plugin['id']} already exists", "error")

View file

@ -1,7 +1,7 @@
from re import match
from threading import Thread
from time import time
from typing import Dict
from typing import Dict, List
from flask import Blueprint, Response, redirect, render_template, request, url_for
from flask_login import login_required
@ -18,6 +18,86 @@ def services_page():
return render_template("services.html", services=DB.get_services(with_drafts=True))
@services.route("/services/convert", methods=["POST"])
@login_required
def services_convert():
verify_data_in_form(
data={"services": None},
err_message="Missing services parameter on /services/convert.",
redirect_url="services",
next=True,
)
verify_data_in_form(
data={"convert_to": None},
err_message="Missing convert_to parameter on /services/convert.",
redirect_url="services",
next=True,
)
services = request.form["services"].split(",")
if not services:
return handle_error("No services selected.", "services", True)
convert_to = request.form["convert_to"]
if convert_to not in ("online", "draft"):
return handle_error("Invalid convert_to parameter.", "services", True)
DATA.load_from_file()
def convert_services(services: List[str], convert_to: str):
wait_applying()
db_services = DB.get_services(with_drafts=True)
services_to_convert = set()
non_ui_services = set()
non_convertible_services = set()
for db_service in db_services:
if db_service["id"] in services:
if db_service["method"] != "ui":
non_ui_services.add(db_service["id"])
continue
if db_service["is_draft"] == (convert_to == "draft"):
non_convertible_services.add(db_service["id"])
continue
services_to_convert.add(db_service["id"])
for non_ui_service in non_ui_services:
DATA["TO_FLASH"].append({"content": f"Service {non_ui_service} is not a UI service and will not be converted.", "type": "error"})
for non_convertible_service in non_convertible_services:
DATA["TO_FLASH"].append(
{"content": f"Service {non_convertible_service} is already a {convert_to} service and will not be converted.", "type": "error"}
)
if not services_to_convert:
DATA["TO_FLASH"].append({"content": "All selected services could not be found, are not UI services or are already converted.", "type": "error"})
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
return
db_config = DB.get_config(with_drafts=True)
for service in services_to_convert:
db_config[f"{service}_IS_DRAFT"] = "yes" if convert_to == "draft" else "no"
ret = DB.save_config(db_config, "ui", changed=True)
if isinstance(ret, str):
DATA["TO_FLASH"].append({"content": ret, "type": "error"})
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
return
DATA["TO_FLASH"].append({"content": f"Converted services: {', '.join(services_to_convert)}", "type": "success"})
DATA["RELOADING"] = False
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
Thread(target=convert_services, args=(services, convert_to)).start()
return redirect(
url_for(
"loading",
next=url_for("services.services_page"),
message=f"Converting service{'s' if len(services) > 1 else ''} {', '.join(services)} to {convert_to}",
)
)
@services.route("/services/delete", methods=["POST"])
@login_required
def services_delete():
@ -32,10 +112,10 @@ def services_delete():
return handle_error("No services selected.", "services", True)
DATA.load_from_file()
def delete_services(services):
def delete_services(services: List[str]):
wait_applying()
db_config = BW_CONFIG.get_config(methods=False)
db_config = BW_CONFIG.get_config(methods=False, with_drafts=True)
db_services = DB.get_services(with_drafts=True)
all_drafts = True
services_to_delete = set()
@ -71,7 +151,7 @@ def services_delete():
DATA["TO_FLASH"].append({"content": ret, "type": "error"})
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
return
DATA["TO_FLASH"].append({"content": f"Deleted services {', '.join(services_to_delete)}", "type": "success"})
DATA["TO_FLASH"].append({"content": f"Deleted services: {', '.join(services_to_delete)}", "type": "success"})
DATA["RELOADING"] = False
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
@ -81,7 +161,7 @@ def services_delete():
url_for(
"loading",
next=url_for("services.services_page"),
message=f"Deleting services {', '.join(services)}",
message=f"Deleting service{'s' if len(services) > 1 else ''} {', '.join(services)}",
)
)
@ -107,7 +187,7 @@ def services_service_page(service: str):
mode = request.args.get("mode", "easy")
is_draft = variables.pop("IS_DRAFT", "no") == "yes"
def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode: str): # TODO: handle easy and raw modes
def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode: str): # TODO: handle easy mode
wait_applying()
# Edit check fields and remove already existing ones
@ -121,15 +201,16 @@ def services_service_page(service: str):
ignored_multiples = set()
# Edit check fields and remove already existing ones
for variable, value in variables.copy().items():
if (mode == "raw" or variable != "SERVER_NAME") and value == config.get(variable, {"value": None})["value"]:
if match(r"^.+_\d+$", variable):
ignored_multiples.add(variable)
del variables[variable]
if mode != "easy":
for variable, value in variables.copy().items():
if (mode == "raw" or variable != "SERVER_NAME") and value == config.get(variable, {"value": None})["value"]:
if match(r"^.+_\d+$", variable):
ignored_multiples.add(variable)
del variables[variable]
variables = BW_CONFIG.check_variables(variables, config, ignored_multiples=ignored_multiples, new=service == "new", threaded=True)
if was_draft == is_draft and not variables:
if service != "new" and was_draft == is_draft and not variables:
content = f"The service {service} was not edited because no values were changed."
DATA["TO_FLASH"].append({"content": content, "type": "warning"})
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
@ -181,6 +262,8 @@ def services_service_page(service: str):
mode = request.args.get("mode", "easy")
search_type = request.args.get("type", "all")
template = request.args.get("template", "high")
db_templates = DB.get_templates()
if service == "new":
clone = request.args.get("clone", "")
if clone:
@ -189,24 +272,30 @@ def services_service_page(service: str):
return render_template(
"service_settings.html",
config=db_config,
templates=db_templates,
mode=mode,
type=search_type,
current_template=template,
)
db_config = DB.get_config(global_only=True, methods=True)
return render_template(
"service_settings.html",
config=db_config,
templates=db_templates,
mode=mode,
type=search_type,
current_template=template,
)
db_config = DB.get_config(methods=True, with_drafts=True, service=service)
return render_template(
"service_settings.html",
config=db_config,
templates=db_templates,
mode=mode,
type=search_type,
current_template=template,
)

View file

@ -453,3 +453,11 @@ a.badge:hover {
background-color: transparent;
opacity: 1; /* You can set this to 0 if you want it to fade out */
}
.template-steps-container {
--bs-breadcrumb-divider: url("../img/bxs-chevron-right.svg");
}
.template-steps-container .breadcrumb-item + .breadcrumb-item::before {
padding: 0 1rem 0 1rem;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10.061 19.061 17.121 12l-7.06-7.061-2.122 2.122L12.879 12l-4.94 4.939z"/></svg>

After

Width:  |  Height:  |  Size: 172 B

View file

@ -309,7 +309,6 @@ $(document).ready(function () {
],
order: [[7, "desc"]],
autoFill: false,
colReorder: true,
responsive: true,
select: {
style: "multi+shift",

View file

@ -293,7 +293,6 @@ $(document).ready(function () {
],
order: [[5, "desc"]],
autoFill: false,
colReorder: true,
responsive: true,
select: {
style: "multi+shift",

View file

@ -1,6 +1,8 @@
$(document).ready(() => {
var toastNum = 0;
let currentPlugin = "general";
let usedTemplate = $("#used-template").val();
let currentTemplate = $("#selected-template").val();
let currentMode = $("#selected-mode").val();
let currentType = $("#selected-type").val();
@ -9,7 +11,10 @@ $(document).ready(() => {
const $pluginTypeSelect = $("#plugin-type-select");
const $pluginKeywordSearch = $("#plugin-keyword-search");
const $pluginDropdownMenu = $("#plugins-dropdown-menu");
const pluginDropdownItems = $("#plugins-dropdown-menu li.nav-item");
const $pluginDropdownItems = $("#plugins-dropdown-menu li.nav-item");
const $templateSearch = $("#template-search");
const $templateDropdownMenu = $("#templates-dropdown-menu");
const $templateDropdownItems = $("#templates-dropdown-menu li.nav-item");
const updateUrlParams = (params, removeHash = false) => {
const newUrl = new URL(window.location.href);
@ -36,37 +41,64 @@ $(document).ready(() => {
// Prepare params for the URL update
const params = {};
if (currentType !== "all") params.type = currentType;
params.mode = currentMode;
if (currentMode === "advanced" && currentType !== "all")
params.type = currentType;
if (currentMode === "easy" && currentTemplate !== "high")
params.template = currentTemplate;
// If "easy" is selected, remove the "mode" parameter
if (currentMode === "easy") {
params.mode = null; // Set mode to null to remove it from the URL
updateUrlParams(params); // Call the function without the hash (keep it intact)
params.type = null; // Remove the type parameter
updateUrlParams(params, true); // Call the function without the hash (keep it intact)
} else {
// If another mode is selected, update the "mode" parameter
params.mode = currentMode;
updateUrlParams(params); // Keep the mode in the URL
params.template = null; // Remove the template parameter
if (currentMode === "advanced" && currentPlugin !== "general") {
// Update the URL hash to the current plugin (e.g., #plugin-name)
window.location.hash = currentPlugin;
} else if (currentMode === "raw") {
params.type = null; // Remove the type parameter
}
updateUrlParams(params, currentMode === "raw"); // Keep the mode in the URL
}
};
const handleTabChange = (targetClass) => {
currentPlugin = targetClass.substring(1).replace("navs-plugins-", "");
// Prepare the params for URL (parameters to be updated in the URL)
const params = {};
if (currentType !== "all") params.type = currentType;
if (currentMode !== "easy") params.mode = currentMode;
if (currentType !== "all") params.type = currentType;
// If "general" is selected and a hash exists, remove the hash but keep the parameters
if (currentPlugin === "general" && window.location.hash) {
// Call updateUrlParams with `removeHash = true` to remove the hash fragment
updateUrlParams(params, true);
} else {
// Update the URL hash to the current plugin (e.g., #plugin-name)
window.location.hash = currentPlugin;
if (targetClass.includes("navs-plugins-")) {
currentPlugin = targetClass.substring(1).replace("navs-plugins-", "");
params.template = null; // Remove the template parameter
// Also update the URL parameters (if any exist) while preserving the hash
updateUrlParams(params);
// If "general" is selected and a hash exists, remove the hash but keep the parameters
if (currentPlugin === "general" && window.location.hash) {
// Call updateUrlParams with `removeHash = true` to remove the hash fragment
updateUrlParams(params, true);
} else {
// Update the URL hash to the current plugin (e.g., #plugin-name)
window.location.hash = currentPlugin;
// Also update the URL parameters (if any exist) while preserving the hash
updateUrlParams(params);
}
} else if (targetClass.includes("navs-templates-")) {
currentTemplate = targetClass.substring(1).replace("navs-templates-", "");
params.type = null; // Remove the type parameter
// If "high" is selected, remove the "template" parameter
if (currentTemplate === "high") {
params.template = null; // Set template to null to remove it from the URL
updateUrlParams(params); // Call the function without the hash (keep it intact)
} else {
// If another template is selected, update the "template" parameter
params.template = currentTemplate;
updateUrlParams(params); // Keep the template in the URL
}
}
};
@ -109,7 +141,70 @@ $(document).ready(() => {
}
};
const getFormFromSettings = () => {
// Function to validate inputs and display error messages
const validateCurrentStepInputs = (currentStepContainer) => {
let isStepValid = true;
currentStepContainer.find(".plugin-setting").each(function () {
const $input = $(this);
const value = $input.val().trim();
const isRequired = $input.prop("required");
const pattern = $input.attr("pattern");
const fieldName =
$input.data("field-name") || $input.attr("name") || "This field";
let errorMessage = "";
let isValid = true;
// Custom error messages
const requiredMessage =
$input.data("required-message") || `${fieldName} is required.`;
const patternMessage =
$input.data("pattern-message") || `Please enter a valid ${fieldName}.`;
// Check if the field is required and not empty
if (isRequired && value === "") {
errorMessage = requiredMessage;
isValid = false;
}
// Validate based on pattern if the input is not empty
if (isValid && pattern && value !== "") {
const regex = new RegExp(pattern);
if (!regex.test(value)) {
errorMessage = patternMessage;
isValid = false;
}
}
// Toggle valid/invalid classes
$input.toggleClass("is-invalid", !isValid);
// Manage the invalid-feedback element
let $feedback = $input.next(".invalid-feedback");
if (!$feedback.length) {
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
$input,
);
}
if (!isValid) {
$feedback.text(errorMessage);
isStepValid = false;
} else {
$feedback.text("");
}
});
if (!isStepValid) {
// Focus the first invalid input
currentStepContainer.find(".is-invalid").first().focus();
}
return isStepValid;
};
const getFormFromSettings = (elem) => {
const form = $("<form>", {
method: "POST",
action: window.location.href,
@ -127,31 +222,48 @@ $(document).ready(() => {
);
};
const addChildrenToForm = (form, elem, isEasy = false) => {
elem.find("input, select").each(function () {
const $this = $(this);
const settingName = $this.attr("name");
const settingType = $this.attr("type");
const originalValue = $this.data("original");
let settingValue = $this.val();
if ($this.is("select")) {
settingValue = $this.val();
} else if (settingType === "checkbox") {
settingValue = $this.is(":checked") ? "yes" : "no";
}
if (!isEasy && settingValue == originalValue) return;
appendHiddenInput(form, settingName, settingValue);
});
};
// Handle missing CSRF token gracefully
const csrfToken = $("#csrf_token").val() || "";
appendHiddenInput(form, "csrf_token", csrfToken);
// TODO: support easy mode
if (currentMode === undefined || currentMode === "advanced") {
$("div[id^='navs-plugins-']")
.find("input, select")
.each(function () {
const $this = $(this);
const settingName = $this.attr("name");
const settingType = $this.attr("type");
const originalValue = $this.data("original");
let settingValue = $this.val();
if (currentMode === "easy") {
const template = elem.data("template");
appendHiddenInput(form, "USE_TEMPLATE", template);
addChildrenToForm(form, $(`#navs-templates-${template}`), true);
if ($this.is("select")) {
settingValue = $this.val();
} else if (settingType === "checkbox") {
settingValue = $this.is(":checked") ? "yes" : "no";
}
// Append 'IS_DRAFT' if it exists
const $draftInput = $("#is-draft");
if ($draftInput.length) {
appendHiddenInput(form, "IS_DRAFT", $draftInput.val());
}
if (settingValue == originalValue) return;
appendHiddenInput(form, settingName, settingValue);
});
// Append 'OLD_SERVER_NAME' if it exists
const $oldServerName = $("#old-server-name");
if ($oldServerName.length) {
appendHiddenInput(form, "OLD_SERVER_NAME", $oldServerName.val());
}
} else if (currentMode === undefined || currentMode === "advanced") {
addChildrenToForm(form, $("div[id^='navs-plugins-']"));
const $draftInput = $("#is-draft");
if ($draftInput.length) {
@ -243,7 +355,7 @@ $(document).ready(() => {
const inputValue = e.target.value.toLowerCase();
let visibleItems = 0;
pluginDropdownItems.each(function () {
$pluginDropdownItems.each(function () {
const item = $(this);
const matches =
(currentType === "all" || item.data("type") === currentType) &&
@ -258,21 +370,54 @@ $(document).ready(() => {
});
if (visibleItems === 0) {
if ($pluginDropdownMenu.find(".no-items").length === 0) {
if ($pluginDropdownMenu.find(".no-plugin-items").length === 0) {
$pluginDropdownMenu.append(
'<li class="no-items dropdown-item text-muted">No Item</li>',
'<li class="no-plugin-items dropdown-item text-muted">No Item</li>',
);
}
} else {
$pluginDropdownMenu.find(".no-items").remove();
$pluginDropdownMenu.find(".no-plugin-items").remove();
}
}, 50),
);
// Clear search and "No Item" message when the dropdown is closed
$("#select-plugin").on("hidden.bs.dropdown", () => {
$("#select-template").on("click", () => $templateSearch.focus());
$("#template-search").on(
"input",
debounce((e) => {
const inputValue = e.target.value.toLowerCase();
let visibleItems = 0;
$templateDropdownItems.each(function () {
const item = $(this);
const matches = item.text().toLowerCase().includes(inputValue);
item.toggle(matches);
if (matches) {
visibleItems++; // Increment when an item is shown
}
});
if (visibleItems === 0) {
if ($templateDropdownMenu.find(".no-template-items").length === 0) {
$templateDropdownMenu.append(
'<li class="no-template-items dropdown-item text-muted">No Item</li>',
);
}
} else {
$templateDropdownMenu.find(".no-template-items").remove();
}
}, 50),
);
$(document).on("hidden.bs.dropdown", "#select-plugin", function () {
$("#plugin-search").val("").trigger("input");
$(".no-items").remove();
});
$(document).on("hidden.bs.dropdown", "#select-template", function () {
$("#template-search").val("").trigger("input");
});
// Attach event listener to handle mode changes when tabs are switched
@ -290,13 +435,22 @@ $(document).ready(() => {
},
);
$('#templates-dropdown-menu button[data-bs-toggle="tab"]').on(
"shown.bs.tab",
(e) => {
handleTabChange($(e.target).data("bs-target"));
},
);
$(document).on("input", ".plugin-setting", function () {
const isValid = $(this).data("pattern")
? new RegExp($(this).data("pattern")).test($(this).val())
: true;
$(this)
.toggleClass("is-valid", isValid)
.toggleClass("is-invalid", !isValid);
debounce(() => {
const isValid = $(this).attr("pattern")
? new RegExp($(this).attr("pattern")).test($(this).val())
: true;
$(this)
.toggleClass("is-valid", isValid)
.toggleClass("is-invalid", !isValid);
}, 100)();
});
$(document).on("focusout", ".plugin-setting", function () {
@ -310,7 +464,7 @@ $(document).ready(() => {
updateUrlParams(params);
pluginDropdownItems.each(function () {
$pluginDropdownItems.each(function () {
const typeMatches =
currentType === "all" || $(this).data("type") === currentType;
$(this).toggle(typeMatches);
@ -325,54 +479,44 @@ $(document).ready(() => {
}
});
const findMatchingSettings = (keyword) => {
let matchedPlugin = null;
let matchedSettings = $();
$("div[id^='navs-plugins-']").each(function () {
const $plugin = $(this);
const pluginId = $plugin.attr("id").replace("navs-plugins-", "");
const pluginType = $plugin.data("type"); // Get the type of the plugin (core, external, pro)
// If the currentType filter is not "all" and the plugin's type doesn't match the currentType, skip this plugin
if (currentType !== "all" && pluginType !== currentType) {
return; // Skip this plugin
}
// Find settings that match the keyword based on label text or input/select name
const matchingSettings = $plugin.find(".form-label").filter(function () {
const $label = $(this);
const settingName = $label.attr("for") || "";
const labelText = $label.text().toLowerCase();
// Find the associated input/select element using the "for" attribute
const $inputElement = $("#" + settingName);
const inputName = $inputElement.attr("name") || "";
// Match either the label text or the input/select name
return (
labelText.includes(keyword) ||
inputName.toLowerCase().includes(keyword)
);
});
if (matchingSettings.length > 0) {
matchedPlugin = pluginId;
matchedSettings = matchingSettings.closest(".col-12");
return false; // Stop searching after finding a plugin with matching settings
}
});
return { matchedPlugin, matchedSettings };
};
$pluginKeywordSearch.on(
"input",
debounce((e) => {
const keyword = e.target.value.toLowerCase().trim();
if (!keyword) return;
const { matchedPlugin, matchedSettings } = findMatchingSettings(keyword);
let matchedPlugin = null;
let matchedSettings = $();
$("div[id^='navs-plugins-']").each(function () {
const $plugin = $(this);
const pluginId = $plugin.attr("id").replace("navs-plugins-", "");
const pluginType = $plugin.data("type"); // Get the type of the plugin (core, external, pro)
// If the currentType filter is not "all" and the plugin's type doesn't match the currentType, skip this plugin
if (currentType !== "all" && pluginType !== currentType) {
return; // Skip this plugin
}
// Find settings that match the keyword based on label text or input/select name
const matchingSettings = $plugin
.find("input, select")
.filter(function () {
const $input = $(this);
const settingName = ($input.attr("name") || "").toLowerCase();
const label = $input.next("label");
const labelText = (label.text() || "").toLowerCase();
// Match either the label text or the input/select name
return labelText.includes(keyword) || settingName.includes(keyword);
});
if (matchingSettings.length > 0) {
matchedPlugin = pluginId;
matchedSettings = matchingSettings.closest(".col-12");
return false; // Stop searching after finding a plugin with matching settings
}
});
if (matchedPlugin) {
// Automatically switch to the plugin tab
@ -586,31 +730,39 @@ $(document).ready(() => {
});
$(".save-settings").on("click", function () {
const form = getFormFromSettings();
// TODO: support easy mode
let minSettings = 4;
if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2;
const form = getFormFromSettings($(this));
if (currentMode === "easy") {
const currentStep = parseInt($(this).data("current-step"));
const template = $(this).data("template");
const currentStepId = `navs-steps-${template}-${currentStep}`;
const currentStepContainer = $(`#${currentStepId}`);
const isStepValid = validateCurrentStepInputs(currentStepContainer);
if (!isStepValid) return;
} else {
let minSettings = 4;
if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2;
const draftInput = $("#is-draft");
const wasDraft = draftInput.data("original") === "yes";
let isDraft = draftInput.val() === "yes";
if (currentMode === "raw")
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
const draftInput = $("#is-draft");
const wasDraft = draftInput.data("original") === "yes";
let isDraft = draftInput.val() === "yes";
if (currentMode === "raw")
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
if (form.children().length < minSettings && isDraft === wasDraft) {
alert("No changes detected.");
return;
if (form.children().length < minSettings && isDraft === wasDraft) {
alert("No changes detected.");
return;
}
}
$(window).off("beforeunload");
form.appendTo("body").submit();
});
$("#toggle-draft").on("click", function () {
$(".toggle-draft").on("click", function () {
const draftInput = $("#is-draft");
const isDraft = draftInput.val() === "yes";
draftInput.val(isDraft ? "no" : "yes");
$(this).html(
$(".toggle-draft").html(
`<i class="bx bx-sm bx-${
isDraft ? "globe" : "file-blank"
} bx-sm"></i>&nbsp;${isDraft ? "Online" : "Draft"}`,
@ -638,6 +790,54 @@ $(document).ready(() => {
});
});
$(document).on("click", ".next-step, .previous-step", function () {
const template = $(this).data("template");
let currentStep = parseInt($(this).data("current-step"));
const isNext = $(this).hasClass("next-step");
// Determine the new step
const newStep = isNext ? currentStep + 1 : currentStep - 1;
const currentStepId = `navs-steps-${template}-${currentStep}`;
const newStepId = `navs-steps-${template}-${newStep}`;
const currentStepContainer = $(`#${currentStepId}`);
const newTabTrigger = $(`button[data-bs-target="#${newStepId}"]`);
const currentTabTrigger = $(`button[data-bs-target="#${currentStepId}"]`);
if (newTabTrigger.length) {
if (isNext) {
const isStepValid = validateCurrentStepInputs(currentStepContainer);
if (!isStepValid) {
// Prevent proceeding to the next step
return;
}
}
currentTabTrigger
.parent()
.find("div.text-primary")
.removeClass("text-primary")
.addClass("text-muted");
currentTabTrigger.addClass("disabled");
// Activate the new tab
const newTab = new bootstrap.Tab(newTabTrigger[0]);
newTab.show();
newTabTrigger
.parent()
.find("div.text-muted")
.removeClass("text-muted")
.addClass("text-primary");
newTabTrigger.removeClass("disabled");
newTabTrigger[0].scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
});
$('div[id^="multiple-"]')
.filter(function () {
return /^multiple-.*-\d+$/.test($(this).attr("id"));
@ -696,9 +896,24 @@ $(document).ready(() => {
}
});
if (
(usedTemplate === "" || usedTemplate === "ui") &&
currentMode === "easy"
) {
$(`button[data-bs-target="#navs-modes-advanced"]`).tab("show");
} else if (usedTemplate !== "high" && currentMode === "easy") {
$(`button[data-bs-target="#navs-templates-${usedTemplate}"]`).tab("show");
}
if (currentMode === "easy" && currentTemplate !== "high") {
$(`button[data-bs-target="#navs-templates-${currentTemplate}"]`).tab(
"show",
);
}
var hasExternalPlugins = false;
var hasProPlugins = false;
pluginDropdownItems.each(function () {
$pluginDropdownItems.each(function () {
const type = $(this).data("type");
if (type === "external") {
hasExternalPlugins = true;
@ -723,11 +938,13 @@ $(document).ready(() => {
if (targetTab.length) targetTab.tab("show");
}
$pluginTypeSelect.trigger("change");
if (currentType !== "all") {
$pluginTypeSelect.trigger("change");
}
if (currentMode === "advanced") {
const serverNameSetting = $("#setting-general-server-name");
if (!serverNameSetting.val()) {
if (serverNameSetting.val() === "") {
if (currentType !== "all") {
currentType = "all";
$pluginTypeSelect.val("all");
@ -766,18 +983,19 @@ $(document).ready(() => {
}
$(window).on("beforeunload", function (e) {
const form = getFormFromSettings();
// TODO: support easy mode
let minSettings = 4;
if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2;
const form = getFormFromSettings($(this));
if (currentMode !== "easy") {
let minSettings = 4;
if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2;
const draftInput = $("#is-draft");
const wasDraft = draftInput.data("original") === "yes";
let isDraft = draftInput.val() === "yes";
if (currentMode === "raw")
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
const draftInput = $("#is-draft");
const wasDraft = draftInput.data("original") === "yes";
let isDraft = draftInput.val() === "yes";
if (currentMode === "raw")
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
if (form.children().length < minSettings && isDraft === wasDraft) return;
if (form.children().length < minSettings && isDraft === wasDraft) return;
}
// Cross-browser compatibility (for older browsers)
var message =

View file

@ -3,6 +3,21 @@
<!-- Content -->
{% set blacklisted_settings = get_blacklisted_settings(true) %}
{% set service_method = "ui" %}
{% set plugin_types = {
"core": {
"icon": "<i class=\"bx bx-cube\"></i>",
"title-class": " border-dark"
},
"external": {
"icon": "<i class=\"bx bx-plug\"></i>",
"title-class": " border-secondary",
"text-class": " text-secondary fw-bold"
},
"pro": {
"title-class": " border-primary",
"text-class": " text-primary fw-bold shine"
}
} %}
{% include "models/plugins_settings.html" %}
<!-- / Content -->
{% endblock %}

View file

@ -73,21 +73,21 @@
</button>
</li>
<div class="collapse w-100 show" id="pluginsCollapse">
{% for plugin in plugins %}
{% with not_pro_pro_plugin = not is_pro_version and plugin['type'] == "pro" %}
{% if not_pro_pro_plugin or plugin['page'] %}
<li class="menu-item{% if current_endpoint == plugin['id'] %} active{% endif %}"{% if not_pro_pro_plugin %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="<i class='bx bx-diamond bx-xs'></i><span>Pro feature</span>"
{% for plugin, plugin_data in plugins.items() %}
{% with not_pro_pro_plugin = not is_pro_version and plugin_data['type'] == "pro" %}
{% if not_pro_pro_plugin or plugin_data['page'] %}
<li class="menu-item{% if current_endpoint == plugin %} active{% endif %}"{% if not_pro_pro_plugin %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="<i class='bx bx-diamond bx-xs'></i><span>Pro feature</span>"
{% endif %}
>
<a href="{% if not_pro_pro_plugin %}https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro{% else %}{{ url_for("plugins") }}/{{ plugin['id'] }}{% endif %}"
<a href="{% if not_pro_pro_plugin %}https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro{% else %}{{ url_for("plugins") }}/{{ plugin }}{% endif %}"
class="menu-link"
{% if not_pro_pro_plugin %}target="_blank" rel="noopener"{% endif %}>
<i class="menu-icon tf-icons bx bx-puzzle"></i>
<div class="text-truncate{% if plugin['type'] == 'pro' %} text-primary shine shine-sm{% endif %} pe-2"
data-i18n="{{ plugin['name'] }}">{{ plugin['name'] }}</div>
{% if plugin['type'] != "pro" %}
<div class="text-truncate{% if plugin_data['type'] == 'pro' %} text-primary shine shine-sm{% endif %} pe-2"
data-i18n="{{ plugin_data['name'] }}">{{ plugin_data['name'] }}</div>
{% if plugin_data['type'] != "pro" %}
<div class="badge rounded-pill bg-label-{% if plugin['type'] == 'core' %}secondary{% else %}primary{% endif %} text-uppercase fs-tiny ms-auto">
{{ plugin['type'].title() }}
{{ plugin_data['type'].title() }}
</div>
{% else %}
<div class="badge badge-center rounded-pill text-uppercase fs-tiny ms-auto">

View file

@ -1,16 +1,15 @@
<div class="form-check form-switch mt-1">
<input id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
<input id="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}"
name="{{ setting }}"
class="form-check-input"
type="checkbox"
role="switch"
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
aria-labelledby="label-{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}"
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
data-default="{{ setting_default }}"
{% if setting_value == "yes" %}checked{% endif %}
{% if disabled %}disabled{% endif %}>
{% if disabled %}disabled{% endif %}
{% if required %}required{% endif %}>
<label class="form-check setting-checkbox-label d-flex align-items-center ps-0"
for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}">
{{ setting }}
</label>
for="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}">{{ setting }}</label>
</div>

View file

@ -1,17 +1,16 @@
<div class="form-floating mt-1{% if setting_data['type'] == 'password' %} input-group input-group-merge form-password-toggle{% endif %}">
<input id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
<input id="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}"
name="{{ setting }}"
type="{{ setting_data['type'] }}"
class="form-control plugin-setting"
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
aria-labelledby="label-{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}"
pattern="{{ setting_data['regex'] }}"
value="{{ setting_value }}"
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
data-default="{{ setting_default }}"
{% if disabled %}disabled{% endif %}>
<label for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}">
{{ setting }}
</label>
{% if disabled %}disabled{% endif %}
{% if required %}required{% endif %}>
<label for="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}">{{ setting }}</label>
{% if setting_data['type'] == 'password' %}
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
{% endif %}

View file

@ -29,15 +29,17 @@
placeholder="Search..."
aria-label="Search...">
</div>
{% for plugin in plugins if get_filtered_settings(plugin["settings"], current_endpoint == "global-config") %}
<li class="nav-item" data-type="{{ plugin['type'] }}">
{% for plugin, plugin_data in plugins.items() if get_filtered_settings(plugin_data["settings"], current_endpoint == "global-config") %}
<li class="nav-item" data-type="{{ plugin_data['type'] }}">
<button type="button"
class="dropdown-item{% if loop.index == 1 %} active{% endif %}"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-plugins-{{ plugin['id'] }}"
aria-controls="navs-plugins-{{ plugin['id'] }}"
{% if loop.index == 1 %}aria-selected="true"{% endif %}>{{ plugin["name"] }}</button>
data-bs-target="#navs-plugins-{{ plugin }}"
aria-controls="navs-plugins-{{ plugin }}"
{% if loop.index == 1 %}aria-selected="true"{% endif %}>
{{ plugin_data["name"] }}
</button>
</li>
{% endfor %}
</ul>
@ -65,15 +67,8 @@
{% if current_endpoint != "global-config" %}
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" title="The draft mode can only be toggled on UI created services"{% endif %}>
{% set is_draft = config.get('IS_DRAFT', {}).get('value', 'no') %}
<input type="hidden"
id="is-draft"
name="IS_DRAFT"
value="{{ is_draft }}"
data-original="{% if current_endpoint != 'new' %}{{ is_draft }}{% else %}no{% endif %}"
data-default="no">
<button id="toggle-draft"
type="button"
class="btn btn-sm btn-outline-secondary me-3 {% if current_endpoint != 'new' and service_method != 'ui' %}disabled{% endif %}">
<button type="button"
class="btn btn-sm btn-outline-secondary toggle-draft me-3 {% if current_endpoint != 'new' and service_method != 'ui' %}disabled{% endif %}">
<i class="bx bx-sm bx-{% if is_draft == 'yes' %}file-blank{% else %}globe{% endif %}"></i>
<span class="d-none d-md-inline">&nbsp;
{% if is_draft == 'yes' %}
@ -95,56 +90,43 @@
</div>
</div>
</div>
{% set plugin_types = {
"core": {
"icon": "<i class=\"bx bx-cube\"></i>",
"title-class": " border-dark"
},
"external": {
"icon": "<i class=\"bx bx-plug\"></i>",
"title-class": " border-secondary text-secondary fw-bold"
},
"pro": {
"title-class": " border-primary text-primary fw-bold shine"
}
} %}
<div class="card tab-content m-1 p-2 position-relative">
{% for plugin in plugins %}
{% set filtered_settings = get_filtered_settings(plugin["settings"], current_endpoint == "global-config") %}
{% for plugin, plugin_data in plugins.items() %}
{% set filtered_settings = get_filtered_settings(plugin_data["settings"], current_endpoint == "global-config") %}
{% if filtered_settings %}
<div id="navs-plugins-{{ plugin['id'] }}"
<div id="navs-plugins-{{ plugin }}"
class="tab-pane fade{% if loop.index == 1 %} show active{% endif %}"
role="tabpanel"
aria-labelledby="navs-plugins-{{ plugin['id'] }}-tab"
data-type="{{ plugin['type'] }}">
aria-labelledby="navs-plugins-{{ plugin }}-tab"
data-type="{{ plugin_data['type'] }}">
<div class="card-header d-flex justify-content-between align-items-center mw-100">
<div class="pt-1 flex-grow-1 me-2" style="min-width: 0;">
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
{{ plugin["name"] }}&nbsp;&nbsp;v{{ plugin["version"] }}&nbsp;&nbsp;{{ plugin_types[plugin["type"]].get('icon', '<img src="' + pro_diamond_url + '"
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin_data['type']].get('text-class', '') }}{{ plugin_types[plugin_data['type']].get('title-class', '') }}">
{{ plugin_data["name"] }}&nbsp;&nbsp;v{{ plugin_data["version"] }}&nbsp;&nbsp;{{ plugin_types[plugin_data["type"]].get('icon', '<img src="' + pro_diamond_url + '"
alt="Pro plugin"
width="18px"
height="15.5px">') |safe }}
</h5>
<p class="card-subtitle text-muted text-truncate mt-2">{{ plugin["description"] }}</p>
<p class="card-subtitle text-muted text-truncate mt-2">{{ plugin_data["description"] }}</p>
</div>
<div class="d-flex flex-grow-0 flex-shrink-0 justify-content-end align-items-center">
<a href="https://docs.bunkerweb.io/latest/quickstart-guide/#protect-udptcp-applications"
class="btn btn-sm btn-{% if plugin['stream'] == 'yes' %}bw-green{% elif plugin['stream'] == 'partial' %}warning{% else %}danger{% endif %} rounded-pill"
class="btn btn-sm btn-{% if plugin_data['stream'] == 'yes' %}bw-green{% elif plugin_data['stream'] == 'partial' %}warning{% else %}danger{% endif %} rounded-pill"
target="_blank"
rel="noopener"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% if plugin['stream'] != 'no' %}Supports{% else %}Doesn't support{% endif %} STREAM mode{% if plugin['stream'] == 'partial' %} partially{% endif %}">
<i class="bx bx-{% if plugin['stream'] == 'yes' %}badge-check{% elif plugin['stream'] == 'partial' %}message-square-detail{% else %}no-entry{% endif %}"></i>&nbsp;STREAM
title="{% if plugin_data['stream'] != 'no' %}Supports{% else %}Doesn't support{% endif %} STREAM mode{% if plugin_data['stream'] == 'partial' %} partially{% endif %}">
<i class="bx bx-{% if plugin_data['stream'] == 'yes' %}badge-check{% elif plugin_data['stream'] == 'partial' %}message-square-detail{% else %}no-entry{% endif %}"></i>&nbsp;STREAM
</a>
<a href="{% if plugin['type'] == 'core' %}https://docs.bunkerweb.io/latest/settings/?utm_campaign=self&utm_source=ui#{% if plugin['id'] == 'general' %}global-settings{% else %}{{ plugin['id'] }}{% endif %}{% else %}https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=ui{% endif %}"
<a href="{% if plugin_data['type'] == 'core' %}https://docs.bunkerweb.io/latest/settings/?utm_campaign=self&utm_source=ui#{% if plugin == 'general' %}global-settings{% else %}{{ plugin }}{% endif %}{% else %}https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=ui{% endif %}"
class="btn btn-sm btn-primary rounded-pill ms-2"
target="_blank"
rel="noopener">
<i class="bx bx-link"></i>&nbsp;More info
</a>
{% if plugin["page"] %}
<a href="{{ url_for("plugins") }}/{{ plugin['id'] }}"
{% if plugin_data["page"] %}
<a href="{{ url_for("plugins") }}/{{ plugin }}"
class="btn btn-sm btn-primary rounded-pill ms-2">
<i class="bx bxs-file-html"></i>&nbsp;Custom page
</a>
@ -153,12 +135,13 @@
</div>
<div class="card-body row pb-0">
{% for setting, setting_data in filtered_settings.items() if not setting_data.get('multiple', false) and setting not in blacklisted_settings and (not service_endpoint or setting_data['context'] == "multisite") %}
{% set setting_id_prefix = "setting-" + plugin + "-" %}
{% set setting_config = config.get(setting, {}) %}
{% set setting_default = setting_data.get("default", "") %}
{% set setting_value = setting_config.get("value", setting_default) %}
{% set setting_method = setting_config.get("method", "default") %}
{% set setting_template = setting_config.get("template", "") %}
{% set disabled = setting_method not in ('ui', 'default') %}
{% set disabled = setting_method not in ('ui', 'default') and (current_endpoint == "global-config" or not setting_config.get("global")) %}
{% if current_endpoint == "new" %}
{% set disabled = false %}
{% if setting == "SERVER_NAME" %}
@ -172,11 +155,9 @@
<div class="col-12 col-sm-6 col-lg-4 pb-3"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="label-{{ plugin['id'] }}-{{ setting_data['id'] }}"
for="setting-{{ plugin['id'] }}-{{ setting_data['id'] }}"
class="form-label fw-semibold text-truncate">
{{ setting_data["label"]|capitalize }}
</label>
<label id="label-setting-{{ plugin }}-{{ setting_data['id'] }}"
for="setting-{{ plugin }}-{{ setting_data['id'] }}"
class="form-label fw-semibold text-truncate">{{ setting_data["label"] }}</label>
<div class="d-flex align-items-center">
{% if current_endpoint == "global-config" and setting_data["context"] == "multisite" %}
<a role="badge"
@ -198,10 +179,18 @@
<span class="bx bx-spreadsheet bx-xs"></span>
</span>
{% endif %}
{% if current_endpoint != "global-config" and setting_config.get("global") and setting_value != setting_default %}
<span class="badge badge-center rounded-pill bg-primary-subtle text-dark d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<span class="badge rounded-pill bg-secondary-subtle text-dark d-flex align-items-center justify-content-center p-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ setting_data['help']|capitalize }}">
title="{{ setting_data['help'] }}">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
@ -224,18 +213,18 @@
</div>
{% set plugin_multiples = get_multiples(filtered_settings, config) %}
{% if plugin_multiples %}
{% set setting_id_prefix = "multiple-" %}
{% set setting_id_prefix = "multiple-setting-" + plugin +"-" %}
{% set multiple_plugin_multiples = plugin_multiples|length > 1 %}
<div class="card-header pb-2 mt-6">
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin_data['type']].get('text-class', '') }}{{ plugin_types[plugin_data['type']].get('title-class', '') }}">
Multiple settings
</h5>
<p class="card-subtitle text-muted mt-2">This is where you can configure multiple settings for this plugin.</p>
</div>
<div class="card-body row card-body pt-0">
<div class="card-body row pt-0">
{% for multiple, multiples in plugin_multiples.items() %}
{% set multiple_settings = settings|length > 1 %}
<div id="multiple-{{ plugin['id'] }}-{{ multiple }}"
<div id="multiple-{{ plugin }}-{{ multiple }}"
class="col-12{% if multiple_plugin_multiples %} col-md-6{% endif %}">
<!-- TODO: Handle if multiple_plugin_multiples|length > 2 and not multiple_settings -> col-lg-4 via JS -->
{% for setting_suffix, settings in multiples.items() %}
@ -248,32 +237,32 @@
</h6>
<div class="d-flex align-items-center">
{% if setting_suffix == "0" %}
<button id="add-multiple-{{ plugin['id'] }}-{{ multiple }}"
<button id="add-multiple-{{ plugin }}-{{ multiple }}"
type="button"
class="btn btn-xs btn-text-bw-green rounded-pill add-multiple p-0 pe-2">
<i class="bx bx-plus-circle bx-sm"></i>&nbsp;ADD
</button>
{% else %}
<div>
<button id="remove-multiple-{{ plugin['id'] }}-{{ multiple }}-{{ setting_suffix }}"
<button id="remove-multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
type="button"
class="btn btn-xs btn-text-danger rounded-pill remove-multiple p-0 pe-2">
<i class="bx bx-trash bx-sm"></i>&nbsp;REMOVE
</button>
</div>
{% endif %}
<button id="show-multiple-{{ plugin['id'] }}-{{ multiple }}-{{ setting_suffix }}"
<button id="show-multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
type="button"
class="btn btn-xs btn-text-secondary rounded-pill show-multiple p-0"
data-bs-toggle="collapse"
data-bs-target="#multiple-{{ plugin['id'] }}-{{ multiple }}-{{ setting_suffix }}"
data-bs-target="#multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
aria-expanded="true"
aria-controls="multiple-{{ plugin['id'] }}-{{ multiple }}-{{ setting_suffix }}">
aria-controls="multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}">
<i class="bx bx-show-alt bx-sm"></i>&nbsp;HIDE
</button>
</div>
</div>
<div id="multiple-{{ plugin['id'] }}-{{ multiple }}-{{ setting_suffix }}"
<div id="multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
class="collapse show multiple-collapse pt-0">
<div class="row mt-2 pt-2">
{% for setting, setting_data in settings.items() if setting not in blacklisted_settings %}
@ -282,7 +271,10 @@
{% set setting_value = setting_config.get("value", setting_default) %}
{% set setting_method = setting_config.get("method", "default") %}
{% set setting_template = setting_config.get("template", "") %}
{% set disabled = current_endpoint != "new" and setting_method not in ('ui', 'default') %}
{% set disabled = setting_method not in ('ui', 'default') and (current_endpoint == "global-config" or not setting_config.get("global")) %}
{% if current_endpoint == "new" %}
{% set disabled = false %}
{% endif %}
{% if service_method == "autoconf" %}
{% set setting_method = "autoconf" %}
{% set disabled = true %}
@ -290,10 +282,10 @@
<div class="col-12{% if multiple_settings %} col-md-6{% endif %}{% if settings|length > 2 and not multiple_multiples %} col-lg-4{% endif %} pb-2"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="multiple-label-{{ plugin['id'] }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
for="multiple-setting-{{ plugin['id'] }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
<label id="label-multiple-setting-{{ plugin }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
for="multiple-setting-{{ plugin }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
class="form-label fw-semibold text-truncate">
{{ setting_data["label"]|capitalize }}
{{ setting_data["label"] }}
</label>
<div class="d-flex align-items-center">
{% if current_endpoint == "global-config" and setting_data["context"] == "multisite" %}
@ -316,10 +308,18 @@
<span class="bx bx-spreadsheet bx-xs"></span>
</span>
{% endif %}
{% if current_endpoint != "global-config" and setting_config.get("global") and setting_value != setting_default %}
<span class="badge badge-center rounded-pill bg-primary-subtle text-dark d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<span class="badge rounded-pill bg-secondary-subtle text-dark d-flex align-items-center justify-content-center p-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ setting_data['help']|capitalize }}">
title="{{ setting_data['help'] }}">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>

View file

@ -0,0 +1,214 @@
{% set plugins_settings = get_plugins_settings() %}
<div class="card p-1 mb-4 sticky-card">
<div class="d-flex flex-wrap justify-content-around align-items-center">
<div class="dropdown btn-group">
<button id="select-template"
type="button"
class="btn btn-outline-primary dropdown-toggle"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="bx bx-notepad"></i>
<span class="d-none d-md-inline">&nbsp;Templates</span>
</button>
<ul id="templates-dropdown-menu"
class="dropdown-menu nav-pills max-vh-60 overflow-auto pt-0"
role="tablist">
<div class="input-group input-group-merge mb-2">
<span class="input-group-text p-2 border-0 border-primary border-bottom shadow-none"><i class="bx fs-6 bx-search"></i></span>
<input id="template-search"
type="text"
class="form-control border-0 border-primary border-bottom shadow-none"
placeholder="Search..."
aria-label="Search...">
</div>
{% for template in templates if template != "ui" %}
<li class="nav-item">
<button type="button"
class="dropdown-item{% if loop.index == 1 %} active{% endif %}"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-templates-{{ template }}"
aria-controls="navs-templates-{{ template }}"
{% if loop.index == 1 %}aria-selected="true"{% endif %}>
{{ template|capitalize }}
</button>
</li>
{% endfor %}
</ul>
</div>
<div class="d-flex justify-content-center">
{% if current_endpoint != "global-config" %}
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" title="The draft mode can only be toggled on UI created services"{% endif %}>
{% set is_draft = config.get('IS_DRAFT', {}).get('value', 'no') %}
<button type="button"
class="btn btn-sm btn-outline-secondary toggle-draft me-3 {% if current_endpoint != 'new' and service_method != 'ui' %}disabled{% endif %}">
<i class="bx bx-sm bx-{% if is_draft == 'yes' %}file-blank{% else %}globe{% endif %}"></i>
<span class="d-none d-md-inline">&nbsp;
{% if is_draft == 'yes' %}
Draft
{% else %}
Online
{% endif %}
</span>
</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="card tab-content m-1 p-2 position-relative">
{% for template, template_data in templates.items() if template != "ui" %}
{% set template_plugin = plugins[template_data['plugin_id']] %}
<div id="navs-templates-{{ template }}"
class="tab-pane fade{% if loop.index == 1 %} show active{% endif %}"
role="tabpanel"
aria-labelledby="navs-templates-{{ template }}-tab">
<div class="card-header d-flex align-items-center mw-100">
<div class="pt-1">
<h5 class="card-title d-inline border p-2{{ plugin_types[template_plugin['type']].get('text-class', '') }}{{ plugin_types[template_plugin['type']].get('title-class', '') }}">
{{ template|capitalize }}&nbsp;&nbsp;{{ plugin_types[template_plugin["type"]].get('icon', '<img src="' + pro_diamond_url + '"
alt="Pro plugin"
width="18px"
height="15.5px">') |safe }}
</h5>
<p class="card-subtitle text-muted text-truncate mt-2">{{ template_data["name"] }}</p>
</div>
</div>
<div class="card-body">
<nav class="p-3 template-steps-container align-items-center mw-100 border rounded-top{{ plugin_types[template_plugin['type']].get('title-class', '') }}"
aria-label="breadcrumb">
<ol class="breadcrumb nav nav-scroller mb-0 flex-nowrap overflow-hidden{% if loop.index == 1 %} active{% endif %}"
role="tablist">
{% for step in template_data["steps"] %}
<li class="breadcrumb-item nav-link d-flex align-items-center pe-0">
<button class="btn btn-primary pt-3 pb-3 me-3{% if loop.index == 1 %} active{% else %} disabled{% endif %}"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-steps-{{ template }}-{{ loop.index }}"
aria-controls="navs-steps-{{ template }}-{{ loop.index }}"
{% if loop.index == 1 %}aria-selected="true"{% endif %}>
{{ loop.index }}
</button>
<div class="text-nowrap">
<div class="fw-bold{% if loop.index == 1 %} text-primary{% else %} text-muted{% endif %}">{{ step["title"] }}</div>
<small class="text-muted">{{ step["subtitle"] }}</small>
</div>
</li>
{% endfor %}
</ol>
</nav>
<div class="tab-content p-3 align-items-center mw-100 border border-top-0 rounded-bottom{{ plugin_types[template_plugin['type']].get('title-class', '') }}">
{% for step in template_data["steps"] %}
<div id="navs-steps-{{ template }}-{{ loop.index }}"
class="ps-2 pe-2 tab-pane fade{% if loop.index == 1 %} show active{% endif %}"
role="tabpanel"
data-step="{{ loop.index }}"
aria-labelledby="navs-steps-{{ template }}-{{ loop.index }}-tab">
<div class="pt-1 pb-4">
<h5 class="mb-1 fw-bold{{ plugin_types[template_plugin['type']].get('text-class', '') }}">{{ step["title"] }}</h5>
<p class="card-subtitle text-muted">{{ step["subtitle"] }}</p>
</div>
<div class="row pb-0">
{% for setting in step["settings"] %}
{% set setting_id_prefix = template + "-setting-" + template_data['plugin_id'] +"-" %}
{% set setting_config = config.get(setting, {}) %}
{% set setting_data = plugins_settings.get(setting, {}) %}
{% set setting_default = setting_data.get("default", "") %}
{% set setting_value = setting_config.get("value", setting_default) %}
{% set setting_method = setting_config.get("method", "default") %}
{% set setting_template = setting_config.get("template", "") %}
{% set disabled = setting_method not in ('ui', 'default') and (current_endpoint == "global-config" or not setting_config.get("global")) %}
{% set required = setting == "SERVER_NAME" %}
{% if current_endpoint == "new" %}
{% set disabled = false %}
{% set setting_value = template_data["settings"].get(setting, setting_default) %}
{% endif %}
{% if service_method == "autoconf" %}
{% set setting_method = "autoconf" %}
{% set disabled = true %}
{% endif %}
<div class="col-12 col-sm-6 col-lg-4 pb-3"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="label-{{ template }}-setting-{{ template_data['plugin_id'] }}-{{ setting_data['id'] }}"
for="{{ template }}-setting-{{ template_data['plugin_id'] }}-{{ setting_data['id'] }}"
class="form-label fw-semibold text-truncate">
{{ setting_data["label"] }}
</label>
<div class="d-flex align-items-center">
{% if current_endpoint == "global-config" and setting_data["context"] == "multisite" %}
<a role="badge"
href='https://docs.bunkerweb.io/latest/concepts/?utm_campaign=self&utm_source=ui#multisite-mode'
class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Multisite setting"
target="_blank"
rel="noopener">
<span class="bx bx-server bx-xs"></span>
</a>
{% endif %}
{% if current_endpoint not in ("new", "global-config") and setting_config.get("global") and setting_value != setting_default %}
<span class="badge badge-center rounded-pill bg-primary-subtle text-dark d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<span class="badge rounded-pill bg-secondary-subtle text-dark d-flex align-items-center justify-content-center p-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ setting_data['help'] }}">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% if setting_data["type"] == "select" %}
{% include "models/select_setting.html" %}
{% elif setting_data["type"] == "check" %}
{% include "models/checkbox_setting.html" %}
{% else %}
{% include "models/input_setting.html" %}
{% endif %}
</div>
{% endfor %}
<div class="col-12 d-flex justify-content-between">
{% if loop.index > 1 %}
<button class="btn btn-primary btn-prev previous-step"
data-template="{{ template }}"
data-current-step="{{ loop.index }}">
<i class="bx bx-chevron-left bx-sm ms-sm-n2"></i>
<span class="align-middle d-sm-inline-block d-none">Previous</span>
</button>
{% else %}
<div></div>
{% endif %}
{% if loop.index == template_data["steps"]|length %}
<div {% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
<button type="button"
class="btn btn-outline-bw-green save-settings{% if service_method == "autoconf" %} disabled{% endif %}"
data-template="{{ template }}"
data-current-step="{{ loop.index }}">
<i class="bx bx-save bx-sm ms-sm-n2"></i>
<span class="align-middle d-sm-inline-block d-none ms-sm-1">Save</span>
</button>
</div>
{% else %}
<button class="btn btn-primary btn-next next-step"
data-template="{{ template }}"
data-current-step="{{ loop.index }}">
<span class="align-middle d-sm-inline-block d-none me-sm-1">Next</span>
<i class="bx bx-chevron-right bx-sm me-sm-n2"></i>
</button>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>

View file

@ -2,17 +2,18 @@
id="csrf_token"
name="csrf_token"
value="{{ csrf_token() }}">
<div class="position-absolute top-0 end-0 m-3"
style="z-index: 1000">
<div class="position-absolute top-0 end-0 m-3" style="z-index: 1000">
<div class="d-flex flex-wrap justify-content-center align-items-center">
<div class="card p-1 me-2">
<button type="button"
class="btn btn-sm btn-outline-secondary copy-settings">
<i class="bx bx-copy-alt bx-xs"></i>
<span class="d-none d-md-inline">&nbsp;Copy</span>
</button>
</div>
<div class="card p-1"{% if service_method == "autoconf" %} data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
{% if request.is_secure %}
<div class="card p-1 me-2">
<button type="button" class="btn btn-sm btn-outline-secondary copy-settings">
<i class="bx bx-copy-alt bx-xs"></i>
<span class="d-none d-md-inline">&nbsp;Copy</span>
</button>
</div>
{% endif %}
<div class="card p-1"
{% if service_method == "autoconf" %} data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
<!-- Save button container -->
<button type="button"
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" %}disabled{% endif %}">
@ -26,8 +27,8 @@
{% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ service_method }}"{% endif %}>
{% set config_lines = ["IS_DRAFT=" + config.get('IS_DRAFT', {}).get('value', 'no')] %}
{% set default_settings = ["IS_DRAFT=no"] %}
{% for plugin in plugins %}
{% set filtered_settings = get_filtered_settings(plugin["settings"], current_endpoint == "global-config") %}
{% for plugin_data in plugins.values() %}
{% set filtered_settings = get_filtered_settings(plugin_data["settings"], current_endpoint == "global-config") %}
{% if filtered_settings %}
{% for setting, setting_data in filtered_settings.items() if setting not in blacklisted_settings %}
{% set setting_config = config.get(setting, {}) %}

View file

@ -1,17 +1,16 @@
<div class="form-floating mt-1">
<select id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
<select id="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}"
name="{{ setting }}"
class="form-select"
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
aria-labelledby="label-{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}"
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
data-default="{{ setting_default }}"
{% if disabled %}disabled{% endif %}>
{% if disabled %}disabled{% endif %}
{% if required %}required{% endif %}>
{% for option in setting_data["select"] %}
<option value="{{ option }}"
{% if setting_value == option %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
<label for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}">
{{ setting }}
</label>
<label for="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}">{{ setting }}</label>
</div>

View file

@ -3,21 +3,45 @@
<!-- Content -->
{% set blacklisted_settings = get_blacklisted_settings() %}
{% set service_method = "ui" %}
{% if current_endpoint != "global-config" %}
{% set service_method = config.get("SERVER_NAME", {"method": "ui"})["method"] %}
<input type="hidden"
id="selected-mode"
name="selected_mode"
value="{{ mode }}">
{% endif %}
{% set plugin_types = {
"core": {
"icon": "<i class=\"bx bx-cube\"></i>",
"title-class": " border-dark"
},
"external": {
"icon": "<i class=\"bx bx-plug\"></i>",
"title-class": " border-secondary",
"text-class": " text-secondary fw-bold"
},
"pro": {
"title-class": " border-primary",
"text-class": " text-primary fw-bold shine"
}
} %}
{% set service_method = config.get("SERVER_NAME", {"method": "ui"})["method"] %}
<input type="hidden"
id="selected-mode"
name="selected_mode"
value="{{ mode }}">
<input type="hidden" id="used-template" name="used_template" value="{% if current_endpoint == "new" %}high{% else %}{{ config.get('USE_TEMPLATE', {'value': ''})['value'] }}{% endif %}">
<input type="hidden"
id="selected-template"
name="selected_template"
value="{{ current_template }}">
<input type="hidden"
id="service-method"
name="service_method"
value="{{ service_method }}">
<input type="hidden"
id="is-draft"
name="IS_DRAFT"
value="{{ is_draft }}"
data-original="{% if current_endpoint != 'new' %}{{ is_draft }}{% else %}no{% endif %}"
data-default="no">
<div class="tab-content p-0 position-relative">
<div class="tab-pane fade{% if mode == 'easy' %} show active{% endif %}"
id="navs-modes-easy"
role="tabpanel">TODO: Add Easy mode</div>
role="tabpanel">{% include "models/plugins_settings_easy.html" %}</div>
<div class="tab-pane fade{% if mode == 'advanced' %} show active{% endif %}"
id="navs-modes-advanced"
role="tabpanel">{% include "models/plugins_settings.html" %}</div>

View file

@ -108,6 +108,7 @@ with app.app_context():
get_multiples=get_multiples,
get_filtered_settings=get_filtered_settings,
get_blacklisted_settings=get_blacklisted_settings,
get_plugins_settings=BW_CONFIG.get_plugins_settings,
url_for=custom_url_for,
)
@ -121,6 +122,9 @@ with app.app_context():
@app.context_processor
def inject_variables():
if request.path.startswith(("/setup", "/loading", "/login", "/totp")):
return dict(script_nonce=app.config["SCRIPT_NONCE"])
DATA.load_from_file()
metadata = DB.get_metadata()