mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Made a lot of improvements in web UI + have a working configuration save for services and global config
This commit is contained in:
parent
3e56a2c522
commit
02a8af2aa7
24 changed files with 976 additions and 422 deletions
|
|
@ -10,5 +10,5 @@ from app.models.ui_database import UIDatabase
|
|||
DB = UIDatabase(getLogger("UI"), log=False)
|
||||
DATA = UIData(Path(sep, "var", "tmp", "bunkerweb").joinpath("ui_data.json"))
|
||||
|
||||
BW_CONFIG = Config(DB)
|
||||
BW_CONFIG = Config(DB, data=DATA)
|
||||
BW_INSTANCES_UTILS = InstancesUtils(DB)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ from pathlib import Path
|
|||
from re import error as RegexError, search as re_search
|
||||
from typing import List, Literal, Optional, Set, Tuple, Union
|
||||
|
||||
from app.utils import get_blacklisted_settings
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, db) -> None:
|
||||
def __init__(self, db, data) -> None:
|
||||
self.__settings = json_loads(Path(sep, "usr", "share", "bunkerweb", "settings.json").read_text(encoding="utf-8"))
|
||||
self.__db = db
|
||||
self.__data = data
|
||||
|
||||
def __gen_conf(
|
||||
self, global_conf: dict, services_conf: list[dict], *, check_changes: bool = True, changed_service: Optional[str] = None
|
||||
|
|
@ -104,7 +107,7 @@ class Config:
|
|||
"""
|
||||
return self.__db.get_services_settings(methods=methods, with_drafts=with_drafts)
|
||||
|
||||
def check_variables(self, variables: dict, config: dict) -> dict:
|
||||
def check_variables(self, variables: dict, config: dict, *, global_config: bool = False, threaded: bool = False) -> dict:
|
||||
"""Testify that the variables passed are valid
|
||||
|
||||
Parameters
|
||||
|
|
@ -117,32 +120,41 @@ class Config:
|
|||
int
|
||||
Return the error code
|
||||
"""
|
||||
self.__data.load_from_file()
|
||||
plugins_settings = self.get_plugins_settings()
|
||||
for k, v in variables.copy().items():
|
||||
check = False
|
||||
|
||||
if k.endswith("SCHEMA"):
|
||||
variables.pop(k)
|
||||
continue
|
||||
|
||||
if k in plugins_settings:
|
||||
setting = k
|
||||
else:
|
||||
setting = k[0 : k.rfind("_")] # noqa: E203
|
||||
if setting not in plugins_settings or "multiple" not in plugins_settings[setting]:
|
||||
flash(f"Variable {k} is not valid.", "error")
|
||||
content = f"Variable {k} is not valid."
|
||||
if threaded:
|
||||
self.__data["TO_FLASH"].append({"content": content, "type": "error"})
|
||||
else:
|
||||
flash(content, "error")
|
||||
variables.pop(k)
|
||||
continue
|
||||
|
||||
if setting in ("AUTOCONF_MODE", "SWARM_MODE", "KUBERNETES_MODE", "IS_LOADING", "IS_DRAFT"):
|
||||
flash(f"Variable {k} is not editable, ignoring it", "error")
|
||||
if setting in get_blacklisted_settings(global_config):
|
||||
message = f"Variable {k} is not editable, ignoring it"
|
||||
if threaded:
|
||||
self.__data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
flash(message, "error")
|
||||
variables.pop(k)
|
||||
continue
|
||||
elif setting not in config and plugins_settings[setting]["default"] == v:
|
||||
variables.pop(k)
|
||||
continue
|
||||
elif config[setting]["method"] not in ("default", "ui"):
|
||||
flash(f"Variable {k} is not editable as is it managed by the {config[setting]['method']}, ignoring it", "error")
|
||||
message = f"Variable {k} is not editable as is it managed by the {config[setting]['method']}, ignoring it"
|
||||
if threaded:
|
||||
self.__data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
flash(message, "error")
|
||||
variables.pop(k)
|
||||
continue
|
||||
|
||||
|
|
@ -150,14 +162,33 @@ class Config:
|
|||
if re_search(plugins_settings[setting]["regex"], v):
|
||||
check = True
|
||||
except RegexError as e:
|
||||
flash(f"Invalid regex for setting {setting} : {plugins_settings[setting]['regex']}, ignoring regex check:{e}", "error")
|
||||
message = f"Invalid regex for setting {setting} : {plugins_settings[setting]['regex']}, ignoring regex check:{e}"
|
||||
if threaded:
|
||||
self.__data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
flash(message, "error")
|
||||
variables.pop(k)
|
||||
continue
|
||||
|
||||
if not check:
|
||||
flash(f"Variable {k} is not valid.", "error")
|
||||
message = f"Variable {k} is not valid."
|
||||
if threaded:
|
||||
self.__data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
flash(message, "error")
|
||||
variables.pop(k)
|
||||
|
||||
for k in config:
|
||||
if k in plugins_settings:
|
||||
continue
|
||||
setting = k[0 : k.rfind("_")] # noqa: E203
|
||||
|
||||
if setting not in plugins_settings or "multiple" not in plugins_settings[setting]:
|
||||
continue
|
||||
|
||||
if k not in variables:
|
||||
variables[k] = plugins_settings[setting]["default"]
|
||||
|
||||
return variables
|
||||
|
||||
def new_service(self, variables: dict, is_draft: bool = False) -> Tuple[str, int]:
|
||||
|
|
|
|||
|
|
@ -95,10 +95,12 @@ class Totp:
|
|||
|
||||
def get_last_counter(self, user: Users) -> Optional[int]:
|
||||
"""Fetch stored last_counter from cache."""
|
||||
DATA.load_from_file()
|
||||
return DATA.get("totp_last_counter", {}).get(user.get_id())
|
||||
|
||||
def set_last_counter(self, user: Users, tmatch: TotpMatch) -> None:
|
||||
"""Cache last_counter."""
|
||||
DATA.load_from_file()
|
||||
if "totp_last_counter" not in DATA:
|
||||
DATA["totp_last_counter"] = {}
|
||||
DATA["totp_last_counter"][user.get_id()] = tmatch.counter
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from bs4 import BeautifulSoup
|
|||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.dependencies import BW_CONFIG, DATA, DB # TODO: remember about DATA.load_from_file()
|
||||
from app.utils import LOGGER, PLUGIN_NAME_RX, path_to_dict
|
||||
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from contextlib import suppress
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Dict
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
|
@ -19,62 +20,66 @@ def global_config_page():
|
|||
if request.method == "POST":
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "global_config")
|
||||
DATA.load_from_file()
|
||||
|
||||
# Check variables
|
||||
variables = request.form.to_dict().copy()
|
||||
del variables["csrf_token"]
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
config = DB.get_config(methods=True, with_drafts=True)
|
||||
services = config["SERVER_NAME"]["value"].split(" ")
|
||||
for variable, value in variables.copy().items():
|
||||
setting = config.get(variable, {"value": None, "global": True})
|
||||
if setting["global"] and value == setting["value"]:
|
||||
del variables[variable]
|
||||
continue
|
||||
|
||||
variables = BW_CONFIG.check_variables(variables, config)
|
||||
|
||||
if not variables:
|
||||
return handle_error("The global configuration was not edited because no values were changed.", "global_config", True)
|
||||
|
||||
for variable, value in variables.copy().items():
|
||||
for service in services:
|
||||
setting = config.get(f"{service}_{variable}", None)
|
||||
if setting and setting["global"] and (setting["value"] != value or setting["value"] == config.get(variable, {"value": None})["value"]):
|
||||
variables[f"{service}_{variable}"] = value
|
||||
|
||||
db_metadata = DB.get_metadata()
|
||||
|
||||
def update_global_config(threaded: bool = False):
|
||||
def update_global_config(variables: Dict[str, str], threaded: bool = False):
|
||||
wait_applying()
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
config = DB.get_config(methods=True, with_drafts=True)
|
||||
services = config["SERVER_NAME"]["value"].split(" ")
|
||||
for variable, value in variables.copy().items():
|
||||
setting = config.get(variable, {"value": None, "global": True})
|
||||
if setting["global"] and value == setting["value"]:
|
||||
del variables[variable]
|
||||
continue
|
||||
|
||||
variables = BW_CONFIG.check_variables(variables, config, global_config=True, threaded=threaded)
|
||||
|
||||
if not variables:
|
||||
content = "The global configuration was not edited because no values were changed."
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": content, "type": "warning"})
|
||||
else:
|
||||
flash(content, "warning")
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
if "PRO_LICENSE_KEY" in variables:
|
||||
DATA["PRO_LOADING"] = True
|
||||
|
||||
for variable, value in variables.copy().items():
|
||||
for service in services:
|
||||
setting = config.get(f"{service}_{variable}", None)
|
||||
if setting and setting["global"] and (setting["value"] != value or setting["value"] == config.get(variable, {"value": None})["value"]):
|
||||
variables[f"{service}_{variable}"] = value
|
||||
|
||||
with suppress(BaseException):
|
||||
if config["PRO_LICENSE_KEY"]["value"] != variables["PRO_LICENSE_KEY"]:
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": "Checking license key to upgrade.", "type": "success"})
|
||||
else:
|
||||
flash("Checking license key to upgrade.", "success")
|
||||
|
||||
manage_bunkerweb("global_config", variables, threaded=threaded)
|
||||
|
||||
if "PRO_LICENSE_KEY" in variables:
|
||||
DATA["PRO_LOADING"] = True
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
Thread(target=update_global_config, args=(variables, True)).start()
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(target=update_global_config, args=(True,)).start()
|
||||
else:
|
||||
update_global_config()
|
||||
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
with suppress(BaseException):
|
||||
if config["PRO_LICENSE_KEY"]["value"] != variables["PRO_LICENSE_KEY"]:
|
||||
flash("Checking license key to upgrade.", "success")
|
||||
arguments = {}
|
||||
if request.args.get("keywords"):
|
||||
arguments["keywords"] = request.args["keywords"]
|
||||
if request.args.get("type", "all") != "all":
|
||||
arguments["type"] = request.args["type"]
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"loading",
|
||||
next=url_for("global_config.global_config_page"),
|
||||
next=url_for("global_config.global_config_page") + f"?{'&'.join([f'{k}={v}' for k, v in arguments.items()])}",
|
||||
message="Saving global configuration",
|
||||
)
|
||||
)
|
||||
|
|
@ -82,5 +87,4 @@ def global_config_page():
|
|||
keywords = request.args.get("keywords", "")
|
||||
search_type = request.args.get("type", "all")
|
||||
global_config = DB.get_config(global_only=True, methods=True)
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
return render_template("global_config.html", config=global_config, plugins=plugins, keywords=keywords, type=search_type)
|
||||
return render_template("global_config.html", config=global_config, keywords=keywords, type=search_type)
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ def instances_action(action: Literal["ping", "reload", "stop", "delete"]): # TO
|
|||
instances = request.form["instances"].split(",")
|
||||
if not instances:
|
||||
return handle_error("No instances selected.", "instances", True)
|
||||
DATA.load_from_file()
|
||||
|
||||
if action == "ping":
|
||||
succeed = []
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from werkzeug.utils import secure_filename
|
|||
|
||||
from common_utils import bytes_hash # type: ignore
|
||||
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB # TODO: remember about DATA.load_from_file()
|
||||
from app.utils import LOGGER, PLUGIN_NAME_RX, TMP_DIR
|
||||
|
||||
from app.routes.utils import PLUGIN_ID_RX, PLUGIN_KEYS, error_message, handle_error, verify_data_in_form, wait_applying
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
from flask import Blueprint, Response, redirect, render_template, request, url_for
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Dict
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import BW_CONFIG, DB
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
|
||||
from app.routes.utils import get_service_data, handle_error, update_service
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, wait_applying
|
||||
|
||||
services = Blueprint("services", __name__)
|
||||
|
||||
|
|
@ -11,15 +14,15 @@ services = Blueprint("services", __name__)
|
|||
@services.route("/services", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def services_page():
|
||||
if request.method == "POST":
|
||||
if request.method == "POST": # TODO: Handle creation and deletion of services
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
|
||||
config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode = get_service_data("services")
|
||||
# config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode = get_service_data("services")
|
||||
|
||||
message = update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged)
|
||||
# message = update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged)
|
||||
|
||||
return redirect(url_for("loading", next=url_for("services.services_page"), message=message))
|
||||
# return redirect(url_for("loading", next=url_for("services.services_page"), message=message))
|
||||
|
||||
return render_template("services.html") # TODO
|
||||
|
||||
|
|
@ -28,11 +31,157 @@ def services_page():
|
|||
@login_required
|
||||
def services_service_page(service: str):
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if service not in services:
|
||||
return Response("Service not found", status=404)
|
||||
service_exists = service in services
|
||||
|
||||
if request.method == "POST":
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
DATA.load_from_file()
|
||||
|
||||
# Check variables
|
||||
variables = request.form.to_dict().copy()
|
||||
del variables["csrf_token"]
|
||||
|
||||
mode = request.args.get("mode", "easy")
|
||||
is_draft = variables.get("IS_DRAFT", "no") == "yes"
|
||||
|
||||
def update_service(variables: Dict[str, str], is_draft: bool, threaded: bool = False): # TODO: handle easy and raw modes
|
||||
wait_applying()
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
if service_exists:
|
||||
config = DB.get_config(methods=True, with_drafts=True, service=service)
|
||||
else:
|
||||
config = DB.get_config(methods=True, with_drafts=True)
|
||||
was_draft = config.get(f"{service}_IS_DRAFT", {"value": "no"})["value"] == "yes"
|
||||
|
||||
old_server_name = variables.pop("OLD_SERVER_NAME", "")
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
for variable, value in variables.copy().items():
|
||||
if variable != "SERVER_NAME" and value == config.get(f"{service}_{variable}", {"value": None})["value"]:
|
||||
del variables[variable]
|
||||
|
||||
variables = BW_CONFIG.check_variables(variables, config, threaded=threaded)
|
||||
|
||||
if was_draft == is_draft and not variables:
|
||||
content = f"The service {service} was not edited because no values were changed."
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": content, "type": "warning"})
|
||||
else:
|
||||
flash(content, "warning")
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
if "SERVER_NAME" not in variables:
|
||||
variables["SERVER_NAME"] = old_server_name
|
||||
|
||||
manage_bunkerweb(
|
||||
"services",
|
||||
variables,
|
||||
old_server_name,
|
||||
operation="edit" if service_exists else "new",
|
||||
is_draft=is_draft,
|
||||
was_draft=was_draft,
|
||||
threaded=threaded,
|
||||
)
|
||||
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
Thread(target=update_service, args=(variables, is_draft, True)).start()
|
||||
|
||||
arguments = {}
|
||||
if mode != "easy":
|
||||
arguments["mode"] = mode
|
||||
if request.args.get("keywords"):
|
||||
arguments["keywords"] = request.args["keywords"]
|
||||
if request.args.get("type", "all") != "all":
|
||||
arguments["type"] = request.args["type"]
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"loading",
|
||||
next=url_for(
|
||||
"services.services_service_page",
|
||||
service=service,
|
||||
)
|
||||
+ f"?{'&'.join([f'{k}={v}' for k, v in arguments.items()])}",
|
||||
message=f"Saving configuration for {'draft ' if is_draft else ''}service {service}",
|
||||
)
|
||||
)
|
||||
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if not service_exists:
|
||||
db_config = DB.get_config(global_only=True, methods=True)
|
||||
return render_template("service_settings.html", config=db_config)
|
||||
|
||||
mode = request.args.get("mode", "easy")
|
||||
keywords = request.args.get("keywords", "")
|
||||
search_type = request.args.get("type", "all")
|
||||
db_config = DB.get_config(methods=True, with_drafts=True, service=service)
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
return render_template("service_settings.html", config=db_config, plugins=plugins, mode=mode, keywords=keywords, type=search_type)
|
||||
return render_template(
|
||||
"service_settings.html",
|
||||
config=db_config,
|
||||
mode=mode,
|
||||
keywords=keywords,
|
||||
type=search_type,
|
||||
)
|
||||
|
||||
|
||||
# def update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged):
|
||||
# if request.form["operation"] == "edit":
|
||||
# if is_draft_unchanged and len(variables) == 1 and "SERVER_NAME" in variables and server_name == old_server_name:
|
||||
# return handle_error("The service was not edited because no values were changed.", "services", True)
|
||||
|
||||
# if request.form["operation"] == "new" and not variables:
|
||||
# return handle_error("The service was not created because all values had the default value.", "services", True)
|
||||
|
||||
# # Delete
|
||||
# if request.form["operation"] == "delete":
|
||||
|
||||
# is_service = BW_CONFIG.check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
|
||||
|
||||
# if not is_service:
|
||||
# error_message(f"Error while deleting the service {request.form['SERVER_NAME']}")
|
||||
|
||||
# if config.get(f"{request.form['SERVER_NAME'].split(' ')[0]}_SERVER_NAME", {"method": "scheduler"})["method"] != "ui":
|
||||
# return handle_error("The service cannot be deleted because it has not been created with the UI.", "services", True)
|
||||
|
||||
# db_metadata = DB.get_metadata()
|
||||
|
||||
# def update_services(threaded: bool = False):
|
||||
# wait_applying()
|
||||
|
||||
# manage_bunkerweb(
|
||||
# "services",
|
||||
# variables,
|
||||
# old_server_name,
|
||||
# variables.get("SERVER_NAME", ""),
|
||||
# operation=operation,
|
||||
# is_draft=is_draft,
|
||||
# was_draft=was_draft,
|
||||
# threaded=threaded,
|
||||
# )
|
||||
|
||||
# if any(
|
||||
# v
|
||||
# for k, v in db_metadata.items()
|
||||
# if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
# ):
|
||||
# DATA["RELOADING"] = True
|
||||
# DATA["LAST_RELOAD"] = time()
|
||||
# Thread(target=update_services, args=(True,)).start()
|
||||
# else:
|
||||
# update_services()
|
||||
|
||||
# DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
# message = ""
|
||||
|
||||
# if request.form["operation"] == "new":
|
||||
# message = f"Creating {'draft ' if is_draft else ''}service {variables.get('SERVER_NAME', '').split(' ')[0]}"
|
||||
# elif request.form["operation"] == "edit":
|
||||
# message = f"Saving configuration for {'draft ' if is_draft else ''}service {old_server_name.split(' ')[0]}"
|
||||
# elif request.form["operation"] == "delete":
|
||||
# message = f"Deleting {'draft ' if was_draft and is_draft else ''}service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
|
||||
|
||||
# return message
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from time import time
|
|||
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.dependencies import BW_CONFIG, DATA, DB # TODO: remember about DATA.load_from_file()
|
||||
from app.utils import USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from app.routes.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ from copy import deepcopy
|
|||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from io import BytesIO
|
||||
from threading import Thread
|
||||
from time import sleep, time
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from flask import Response, flash, redirect, request, session, url_for
|
||||
|
|
@ -49,6 +48,7 @@ def wait_applying():
|
|||
def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: bool = False, was_draft: bool = False, threaded: bool = False) -> int:
|
||||
# Do the operation
|
||||
error = 0
|
||||
DATA.load_from_file()
|
||||
|
||||
if "TO_FLASH" not in DATA:
|
||||
DATA["TO_FLASH"] = []
|
||||
|
|
@ -311,66 +311,6 @@ def get_service_data(page_name: str):
|
|||
return config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode
|
||||
|
||||
|
||||
def update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged):
|
||||
if request.form["operation"] == "edit":
|
||||
if is_draft_unchanged and len(variables) == 1 and "SERVER_NAME" in variables and server_name == old_server_name:
|
||||
return handle_error("The service was not edited because no values were changed.", "services", True)
|
||||
|
||||
if request.form["operation"] == "new" and not variables:
|
||||
return handle_error("The service was not created because all values had the default value.", "services", True)
|
||||
|
||||
# Delete
|
||||
if request.form["operation"] == "delete":
|
||||
|
||||
is_service = BW_CONFIG.check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
|
||||
|
||||
if not is_service:
|
||||
error_message(f"Error while deleting the service {request.form['SERVER_NAME']}")
|
||||
|
||||
if config.get(f"{request.form['SERVER_NAME'].split(' ')[0]}_SERVER_NAME", {"method": "scheduler"})["method"] != "ui":
|
||||
return handle_error("The service cannot be deleted because it has not been created with the UI.", "services", True)
|
||||
|
||||
db_metadata = DB.get_metadata()
|
||||
|
||||
def update_services(threaded: bool = False):
|
||||
wait_applying()
|
||||
|
||||
manage_bunkerweb(
|
||||
"services",
|
||||
variables,
|
||||
old_server_name,
|
||||
variables.get("SERVER_NAME", ""),
|
||||
operation=operation,
|
||||
is_draft=is_draft,
|
||||
was_draft=was_draft,
|
||||
threaded=threaded,
|
||||
)
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(target=update_services, args=(True,)).start()
|
||||
else:
|
||||
update_services()
|
||||
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
message = ""
|
||||
|
||||
if request.form["operation"] == "new":
|
||||
message = f"Creating {'draft ' if is_draft else ''}service {variables.get('SERVER_NAME', '').split(' ')[0]}"
|
||||
elif request.form["operation"] == "edit":
|
||||
message = f"Saving configuration for {'draft ' if is_draft else ''}service {old_server_name.split(' ')[0]}"
|
||||
elif request.form["operation"] == "delete":
|
||||
message = f"Deleting {'draft ' if was_draft and is_draft else ''}service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def cors_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -290,6 +290,23 @@ td.highlight {
|
|||
background-color: var(--bs-bw-green);
|
||||
}
|
||||
|
||||
.btn-text-bw-green,
|
||||
.btn-text-bw-green:hover {
|
||||
color: var(--bs-bw-green) !important;
|
||||
}
|
||||
|
||||
.btn-text-secondary,
|
||||
.btn-text-secondary:hover {
|
||||
color: var(--bs-secondary) !important;
|
||||
}
|
||||
|
||||
.btn-text-danger,
|
||||
.btn-text-danger.disabled,
|
||||
.btn-text-danger:hover {
|
||||
color: var(--bs-danger) !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.col.form-floating > .form-control-sm,
|
||||
.col-auto.form-floating > .form-select.form-select-sm {
|
||||
height: calc(1.9em + 0.9rem + 1px);
|
||||
|
|
@ -398,3 +415,17 @@ a.badge:hover {
|
|||
.setting-checkbox-label {
|
||||
font-size: calc(var(--bs-body-font-size) * 0.85);
|
||||
}
|
||||
|
||||
.sticky-card {
|
||||
position: sticky;
|
||||
background-color: rgba(255, 255, 255, 0.88) !important;
|
||||
backdrop-filter: saturate(200%) blur(6px);
|
||||
top: 85px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#floating-modes-menu {
|
||||
bottom: 2.1rem !important;
|
||||
left: 1.5rem !important;
|
||||
z-index: 1080;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ $(document).ready(function () {
|
|||
|
||||
// Create a FormData object
|
||||
const formData = new FormData();
|
||||
formData.append("csrf_token", $("#csrf_token").val()); // Add the CSRF token
|
||||
formData.append("csrf_token", $("#csrf_token").val()); // Add the CSRF token // TODO: find a way to ignore CSRF token
|
||||
formData.append("instances", instances.join(",")); // Add the instances
|
||||
|
||||
// Send the form data using $.ajax
|
||||
|
|
@ -224,21 +224,21 @@ $(document).ready(function () {
|
|||
}
|
||||
|
||||
// Create a form element using jQuery and set its attributes
|
||||
const $form = $("<form>", {
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: `${window.location.pathname}/${action}`,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
// Add CSRF token and instances as hidden inputs
|
||||
$form.append(
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
}),
|
||||
);
|
||||
$form.append(
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "instances",
|
||||
|
|
@ -247,7 +247,7 @@ $(document).ready(function () {
|
|||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
$form.appendTo("body").submit();
|
||||
form.appendTo("body").submit();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ $(document).ready(() => {
|
|||
let currentKeywords = "";
|
||||
const pluginDropdownItems = $("#plugins-dropdown-menu li.nav-item");
|
||||
|
||||
const updateUrlParams = (params) => {
|
||||
const updateUrlParams = (params, removeHash = false) => {
|
||||
// Create a new URL based on the current location (this keeps both the search params and the hash)
|
||||
const newUrl = new URL(window.location.href);
|
||||
|
||||
|
|
@ -20,7 +20,12 @@ $(document).ready(() => {
|
|||
// Update the search params of the URL
|
||||
newUrl.search = searchParams.toString();
|
||||
|
||||
// Push the updated URL (this keeps the hash and the merged search params)
|
||||
// Optionally remove the hash from the URL
|
||||
if (removeHash) {
|
||||
newUrl.hash = "";
|
||||
}
|
||||
|
||||
// Push the updated URL (this keeps or removes the hash and updates the search params)
|
||||
history.pushState(params, document.title, newUrl.toString());
|
||||
};
|
||||
|
||||
|
|
@ -88,7 +93,8 @@ $(document).ready(() => {
|
|||
const params = {};
|
||||
if (currentType !== "all") params.type = currentType;
|
||||
if (currentMode !== "easy") params.mode = currentMode;
|
||||
updateUrlParams(params);
|
||||
// Call updateUrlParams with `removeHash = true` to remove the hash
|
||||
updateUrlParams(params, true);
|
||||
} else {
|
||||
window.location.hash = currentPlugin;
|
||||
}
|
||||
|
|
@ -114,45 +120,6 @@ $(document).ready(() => {
|
|||
$(this).removeClass("is-valid");
|
||||
});
|
||||
|
||||
$(".show-multiple").on("click", function () {
|
||||
const toggleText = $(this).text().trim() === "SHOW" ? "HIDE" : "SHOW";
|
||||
$(this).html(
|
||||
`<i class="bx bx-${
|
||||
toggleText === "SHOW" ? "hide" : "show-alt"
|
||||
} bx-sm"></i> ${toggleText}`,
|
||||
);
|
||||
});
|
||||
|
||||
$(".add-multiple").on("click", function () {
|
||||
const showButtonId = $(this).attr("id").replace("add", "show");
|
||||
if ($(`#${showButtonId}`).text().trim() === "SHOW") {
|
||||
$(`#${showButtonId}`).trigger("click");
|
||||
}
|
||||
});
|
||||
|
||||
$('div[id^="multiple-"]')
|
||||
.filter(function () {
|
||||
return !/^multiple-.*-\d+$/.test($(this).attr("id"));
|
||||
})
|
||||
.each(function () {
|
||||
let defaultValues = true;
|
||||
$(this)
|
||||
.find("input, select")
|
||||
.each(function () {
|
||||
const type = $(this).attr("type");
|
||||
const defaultVal = $(this).data("default");
|
||||
const isChecked =
|
||||
type === "checkbox" &&
|
||||
$(this).prop("checked") === (defaultVal === "yes");
|
||||
const isMatchingValue =
|
||||
type !== "checkbox" && $(this).val() === defaultVal;
|
||||
|
||||
if (!isChecked && !isMatchingValue) defaultValues = false;
|
||||
});
|
||||
|
||||
if (defaultValues) $(`#show-${$(this).attr("id")}`).trigger("click");
|
||||
});
|
||||
|
||||
$("#plugin-type-select").on("change", function () {
|
||||
currentType = $(this).val();
|
||||
const params = currentType === "all" ? {} : { type: currentType };
|
||||
|
|
@ -174,6 +141,317 @@ $(document).ready(() => {
|
|||
}
|
||||
});
|
||||
|
||||
$(document).on("click", ".show-multiple", function () {
|
||||
const toggleText = $(this).text().trim() === "SHOW" ? "HIDE" : "SHOW";
|
||||
$(this).html(
|
||||
`<i class="bx bx-${
|
||||
toggleText === "SHOW" ? "hide" : "show-alt"
|
||||
} bx-sm"></i> ${toggleText}`,
|
||||
);
|
||||
});
|
||||
|
||||
$(".add-multiple").on("click", function () {
|
||||
const multipleId = $(this).attr("id").replace("add-", "");
|
||||
const suffix = $(`#${multipleId}`).find(".multiple-container").length;
|
||||
const cloneId = `${multipleId}-${suffix}`;
|
||||
|
||||
// Clone the first .multiple-container and reset input values
|
||||
const multipleClone = $(`#${multipleId}`)
|
||||
.find(".multiple-container")
|
||||
.first()
|
||||
.clone();
|
||||
|
||||
// Update the IDs and names of the cloned inputs/selects
|
||||
multipleClone.find("input, select").each(function () {
|
||||
const type = $(this).attr("type");
|
||||
const defaultVal = $(this).data("default");
|
||||
|
||||
// Enable the inputs/selects and update values
|
||||
$(this).attr("disabled", false);
|
||||
const newId = $(this).attr("id").replace("-0", `-${suffix}`);
|
||||
const newName = `${$(this).attr("name")}_${suffix}`;
|
||||
$(this).attr("id", newId).attr("name", newName);
|
||||
|
||||
// Update the label for the input/select
|
||||
const settingLabel = $(this).next("label");
|
||||
settingLabel.attr("for", newId).text(`${settingLabel.text()}_${suffix}`);
|
||||
|
||||
// Update value to an empty string or default value
|
||||
if ($(this).is("select")) {
|
||||
$(this).val(defaultVal);
|
||||
$(this)
|
||||
.find("option")
|
||||
.each(function () {
|
||||
$(this).prop("selected", false);
|
||||
});
|
||||
} else if (type === "checkbox") {
|
||||
$(this).prop("checked", false);
|
||||
} else {
|
||||
$(this).val("");
|
||||
}
|
||||
});
|
||||
|
||||
// Update the collapse section's ID and remove tooltips
|
||||
multipleClone
|
||||
.find(".multiple-collapse")
|
||||
.attr("id", `${cloneId}`)
|
||||
.find('[data-bs-toggle="tooltip"]:not(.badge)')
|
||||
.each(function () {
|
||||
$(this)
|
||||
.removeAttr("data-bs-toggle")
|
||||
.removeAttr("data-bs-placement")
|
||||
.removeAttr("data-bs-original-title");
|
||||
});
|
||||
|
||||
// Add the #suffix to h6
|
||||
const multipleTitle = multipleClone.find("h6");
|
||||
multipleTitle.text(`${multipleTitle.text()} #${suffix}`);
|
||||
|
||||
// Append the "REMOVE" button
|
||||
multipleClone.find(".add-multiple").remove();
|
||||
multipleClone.find(".show-multiple").before(
|
||||
`<div>
|
||||
<button id="remove-${cloneId}"
|
||||
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> REMOVE
|
||||
</button>
|
||||
</div>`,
|
||||
);
|
||||
|
||||
// Append the cloned element to the container
|
||||
$(`#${multipleId}`).append(multipleClone);
|
||||
|
||||
// Reinitialize Bootstrap tooltips for the newly added clone
|
||||
multipleClone.find('[data-bs-toggle="tooltip"]').tooltip();
|
||||
|
||||
// Update the data-bs-target and aria-controls attributes of the show-multiple button
|
||||
const showMultiple = multipleClone.find(".show-multiple");
|
||||
showMultiple
|
||||
.attr("data-bs-target", `#${cloneId}`)
|
||||
.attr("aria-controls", cloneId);
|
||||
if (showMultiple.text().trim() === "SHOW") showMultiple.trigger("click");
|
||||
|
||||
// Scroll to the newly added element
|
||||
multipleClone.focus()[0].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("click", ".remove-multiple", function () {
|
||||
const multipleId = $(this).attr("id").replace("remove-", "");
|
||||
const multiple = $(`#${multipleId}`);
|
||||
|
||||
// Check if any input/select is disabled, and exit early if so
|
||||
let disabled = false;
|
||||
multiple.find("input, select").each(function () {
|
||||
if ($(this).prop("disabled")) {
|
||||
disabled = true;
|
||||
return false; // Exit the loop early
|
||||
}
|
||||
});
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
const elementToRemove = multiple.parent();
|
||||
|
||||
// Ensure the element has the 'collapse' class
|
||||
if (!elementToRemove.hasClass("collapse")) {
|
||||
elementToRemove.addClass("collapse show");
|
||||
}
|
||||
|
||||
// Initialize Bootstrap Collapse for the element
|
||||
const bsCollapse = new bootstrap.Collapse(elementToRemove, {
|
||||
toggle: false, // Ensure we only collapse, not toggle
|
||||
});
|
||||
|
||||
// Start the collapsing animation and adjust padding
|
||||
bsCollapse.hide();
|
||||
elementToRemove.removeClass("pt-2 pb-2").addClass("pt-0 pb-0");
|
||||
|
||||
// Remove the element after collapse transition completes
|
||||
elementToRemove.on("hidden.bs.collapse", function () {
|
||||
setTimeout(() => {
|
||||
$(this).remove(); // Remove the element after collapse
|
||||
}, 60);
|
||||
});
|
||||
|
||||
// Update all next elements' IDs and names
|
||||
elementToRemove.nextAll().each(function () {
|
||||
const nextId = $(this).find(".multiple-collapse").attr("id");
|
||||
const nextSuffix = parseInt(
|
||||
nextId.substring(nextId.lastIndexOf("-") + 1),
|
||||
10,
|
||||
);
|
||||
const newSuffix = nextSuffix - 1;
|
||||
const newId = nextId.replace(`-${nextSuffix}`, `-${newSuffix}`);
|
||||
|
||||
// Update the ID of the next element
|
||||
$(this).find(".multiple-collapse").attr("id", newId);
|
||||
|
||||
const multipleTitle = $(this).find("h6");
|
||||
multipleTitle.text(function () {
|
||||
return $(this).text().replace(` #${nextSuffix}`, ` #${newSuffix}`);
|
||||
});
|
||||
|
||||
// Update the input/select name and corresponding label
|
||||
$(this)
|
||||
.find("input, select")
|
||||
.each(function () {
|
||||
const newName = $(this)
|
||||
.attr("name")
|
||||
.replace(`_${nextSuffix}`, `_${newSuffix}`);
|
||||
$(this).attr("name", newName);
|
||||
|
||||
// Find the associated label and update its 'for' attribute and text
|
||||
const settingLabel = $(`label[for="${$(this).attr("id")}"]`);
|
||||
if (settingLabel.length) {
|
||||
settingLabel.attr("for", newId).text(function () {
|
||||
return $(this).text().replace(`_${nextSuffix}`, `_${newSuffix}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update the data-bs-target and aria-controls of the show-multiple button
|
||||
const showMultiple = $(this).find(".show-multiple");
|
||||
showMultiple
|
||||
.attr("data-bs-target", `#${newId}`)
|
||||
.attr("aria-controls", newId);
|
||||
|
||||
const removeMultiple = $(this).find(".remove-multiple");
|
||||
removeMultiple.attr("id", `remove-${newId}`);
|
||||
});
|
||||
});
|
||||
|
||||
$("#save-settings").on("click", function () {
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: window.location.href,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
}),
|
||||
);
|
||||
$("div[id^='navs-plugins-']")
|
||||
.find("input, select")
|
||||
.each(function () {
|
||||
const settingName = $(this).attr("name");
|
||||
const settingType = $(this).attr("type");
|
||||
const originalValue = $(this).data("original");
|
||||
var settingValue = $(this).val();
|
||||
|
||||
if ($(this).is("select")) {
|
||||
settingValue = $(this).find("option:selected").val();
|
||||
} else if (settingType === "checkbox") {
|
||||
settingValue = $(this).prop("checked") ? "yes" : "no";
|
||||
}
|
||||
|
||||
if (
|
||||
$(this).attr("id") &&
|
||||
!$(this).attr("id").startsWith("multiple-") &&
|
||||
settingValue == originalValue
|
||||
)
|
||||
return;
|
||||
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: settingName,
|
||||
value: settingValue,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
if (form.children().length < 2) {
|
||||
alert("No changes detected.");
|
||||
return;
|
||||
}
|
||||
form.appendTo("body").submit();
|
||||
});
|
||||
|
||||
$('div[id^="multiple-"]')
|
||||
.filter(function () {
|
||||
return /^multiple-.*-\d+$/.test($(this).attr("id"));
|
||||
})
|
||||
.each(function () {
|
||||
let defaultValues = true;
|
||||
let disabled = false;
|
||||
$(this)
|
||||
.find("input, select")
|
||||
.each(function () {
|
||||
const type = $(this).attr("type");
|
||||
const defaultVal = $(this).data("default");
|
||||
|
||||
if ($(this).prop("disabled")) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
// Check for select element
|
||||
if ($(this).is("select")) {
|
||||
const selectedVal = $(this).find("option:selected").val();
|
||||
if (selectedVal != defaultVal) {
|
||||
defaultValues = false;
|
||||
}
|
||||
} else if (type === "checkbox") {
|
||||
const isChecked =
|
||||
$(this).prop("checked") === (defaultVal === "yes");
|
||||
if (!isChecked) {
|
||||
defaultValues = false;
|
||||
}
|
||||
} else {
|
||||
const isMatchingValue = $(this).val() == defaultVal;
|
||||
if (!isMatchingValue) {
|
||||
defaultValues = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (defaultValues) $(`#show-${$(this).attr("id")}`).trigger("click");
|
||||
if (disabled && $(`#remove-${$(this).attr("id")}`).length) {
|
||||
$(`#remove-${$(this).attr("id")}`).addClass("disabled");
|
||||
$(`#remove-${$(this).attr("id")}`)
|
||||
.parent()
|
||||
.attr(
|
||||
"title",
|
||||
"Cannot remove because one or more settings are disabled",
|
||||
);
|
||||
|
||||
new bootstrap.Tooltip(
|
||||
$(`#remove-${$(this).attr("id")}`)
|
||||
.parent()
|
||||
.get(0),
|
||||
{
|
||||
placement: "top",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var hasExternalPlugins = false;
|
||||
var hasProPlugins = false;
|
||||
pluginDropdownItems.each(function () {
|
||||
const type = $(this).data("type");
|
||||
if (type === "external") {
|
||||
hasExternalPlugins = true;
|
||||
} else if (type === "pro") {
|
||||
hasProPlugins = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasExternalPlugins && !hasProPlugins) {
|
||||
$("#plugin-type-select").parent().remove();
|
||||
} else if (!hasExternalPlugins) {
|
||||
$("#plugin-type-select option[value='external']").remove();
|
||||
} else if (!hasProPlugins) {
|
||||
$("#plugin-type-select option[value='pro']").remove();
|
||||
}
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const targetTab = $(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
<nav class="d-flex align-items-center" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<!-- prettier-ignore -->
|
||||
{% set breadcrumbs_url = url_for(request.endpoint) %}
|
||||
{% if current_endpoint != "services" and "services" in request.path %}
|
||||
{% set breadcrumbs_url = url_for("services") + "/" + current_endpoint %}
|
||||
{% endif %}
|
||||
{% with breadcrumbs = breadcrumbs_url.split("/") %}
|
||||
{% for breadcrumb in breadcrumbs[1:] %}
|
||||
{% with breadcrumbs = request.path.split("/") %}
|
||||
{% for breadcrumb in breadcrumbs %}
|
||||
{% set breadcrumb_url = url_for(breadcrumb.replace('_', '-')) %}
|
||||
{% if breadcrumb == current_endpoint %}
|
||||
{% set breadcrumb_url = breadcrumbs_url %}
|
||||
|
|
|
|||
|
|
@ -14,44 +14,103 @@
|
|||
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||
{% include "breadcrumb.html" %}
|
||||
{% if current_endpoint != "services" and "services" in request.path %}
|
||||
<!-- Floating button to toggle the menu -->
|
||||
<div class="position-fixed bottom-0 start-0 m-3 d-lg-none"
|
||||
id="floating-modes-menu">
|
||||
<button class="btn btn-sm btn-primary d-flex align-items-center justify-content-center rounded-pill me-1"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#service-modes-floating"
|
||||
aria-controls="service-modes-floating"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<i class="bx bx-xs bx-menu"></i> Service Modes
|
||||
</button>
|
||||
<!-- Collapsible floating menu -->
|
||||
<div class="collapse mt-2" id="service-modes-floating">
|
||||
<ul id="service-modes-menu"
|
||||
class="nav nav-pills flex-column bg-white p-2 rounded shadow-sm"
|
||||
role="tablist">
|
||||
<li class="nav-item mb-1" role="presentation">
|
||||
<button type="button"
|
||||
class="btn btn-sm rounded-pill nav-link d-flex align-items-center{% if mode == 'easy' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-modes-easy"
|
||||
aria-controls="navs-modes-easy"
|
||||
{% if mode == 'easy' %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-customize bx-sm"></i>
|
||||
|
||||
<span>Easy</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item mb-1" role="presentation">
|
||||
<button type="button"
|
||||
class="btn btn-sm rounded-pill nav-link d-flex align-items-center{% if mode == 'advanced' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-modes-advanced"
|
||||
aria-controls="navs-modes-advanced"
|
||||
{% if mode == 'advanced' %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-shield-quarter bx-sm"></i>
|
||||
|
||||
<span>Advanced</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="btn btn-sm rounded-pill nav-link d-flex align-items-center{% if mode == 'raw' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-modes-raw"
|
||||
aria-controls="navs-modes-raw"
|
||||
{% if mode == 'raw' %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-notepad bx-sm"></i>
|
||||
|
||||
<span>Raw</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="service-modes-menu"
|
||||
class="nav nav-pills flex-column flex-md-row"
|
||||
class="nav nav-pills flex-column flex-md-row d-none d-lg-flex"
|
||||
role="tablist">
|
||||
<li class="nav-item me-0 me-sm-3" role="presentation">
|
||||
<button type="button"
|
||||
class="rounded-pill nav-link d-flex align-items-center{% if mode == 'easy' %} active{% endif %}"
|
||||
class="btn btn-sm rounded-pill nav-link d-flex align-items-center{% if mode == 'easy' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-modes-easy"
|
||||
aria-controls="navs-modes-easy"
|
||||
{% if mode == 'easy' %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-customize bx-sm"></i>
|
||||
<i class="bx bx-customize bx-xs"></i>
|
||||
|
||||
<span class="d-none d-sm-inline">Easy</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item me-0 me-sm-3" role="presentation">
|
||||
<button type="button"
|
||||
class="rounded-pill nav-link d-flex align-items-center{% if mode == 'advanced' %} active{% endif %}"
|
||||
class="btn btn-sm rounded-pill nav-link d-flex align-items-center{% if mode == 'advanced' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-modes-advanced"
|
||||
aria-controls="navs-modes-advanced"
|
||||
{% if mode == 'advanced' %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-shield-quarter bx-sm"></i>
|
||||
<i class="bx bx-shield-quarter bx-xs"></i>
|
||||
|
||||
<span class="d-none d-sm-inline">Advanced</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="rounded-pill nav-link d-flex align-items-center{% if mode == 'raw' %} active{% endif %}"
|
||||
class="btn btn-sm rounded-pill nav-link d-flex align-items-center{% if mode == 'raw' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-modes-raw"
|
||||
aria-controls="navs-modes-raw"
|
||||
{% if mode == 'raw' %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-notepad bx-sm"></i>
|
||||
<i class="bx bx-notepad bx-xs"></i>
|
||||
|
||||
<span class="d-none d-sm-inline">Raw</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
<div class="toast-header">
|
||||
<i class="d-block w-px-20 h-auto rounded me-2 tf-icons bx bx-bell"></i>
|
||||
<span class="fw-medium me-auto">
|
||||
{% if category == 'error' %}
|
||||
Error
|
||||
{% if category != 'message' %}
|
||||
{{ category|capitalize }}
|
||||
{% else %}
|
||||
Success
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -72,61 +72,59 @@
|
|||
<i class="bx bx-chevron-right chevron-icon chevron-rotate"></i>
|
||||
</button>
|
||||
</li>
|
||||
<div class="collapse show" id="pluginsCollapse">
|
||||
<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 %}">
|
||||
<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 %}"
|
||||
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 %}"
|
||||
data-i18n="{{ plugin['name'] }}">{{ plugin['name'] }}</div>
|
||||
{% if plugin['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() }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="badge badge-center rounded-pill text-uppercase fs-tiny ms-auto"
|
||||
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>">
|
||||
<img src="{{ pro_diamond_url }}"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- / Plugins Pages -->
|
||||
<!-- Misc -->
|
||||
<li class="menu-header small text-uppercase">
|
||||
<span class="menu-header-text">Misc</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://panel.bunkerweb.io/order/support/?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-support"></i>
|
||||
<div class="text-truncate" data-i18n="Support">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://docs.bunkerweb.io/latest/?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-file"></i>
|
||||
<div class="text-truncate" data-i18n="Documentation">Documentation</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endwith %}
|
||||
<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>"
|
||||
{% 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 %}"
|
||||
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="badge rounded-pill bg-label-{% if plugin['type'] == 'core' %}secondary{% else %}primary{% endif %} text-uppercase fs-tiny ms-auto">
|
||||
{{ plugin['type'].title() }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="badge badge-center rounded-pill text-uppercase fs-tiny ms-auto">
|
||||
<img src="{{ pro_diamond_url }}"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- / Plugins Pages -->
|
||||
<!-- Misc -->
|
||||
<li class="menu-header small text-uppercase">
|
||||
<span class="menu-header-text">Misc</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://panel.bunkerweb.io/order/support/?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-support"></i>
|
||||
<div class="text-truncate" data-i18n="Support">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://docs.bunkerweb.io/latest/?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-file"></i>
|
||||
<div class="text-truncate" data-i18n="Documentation">Documentation</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endwith %}
|
||||
</aside>
|
||||
<!-- / Menu -->
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<div class="form-check form-switch mt-1">
|
||||
<input id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
<input id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
name="{{ setting }}"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
data-default="{{ setting_data['default'] }}"
|
||||
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
data-original="{{ setting_value }}"
|
||||
data-default="{{ setting_default }}"
|
||||
{% if setting_value == "yes" %}checked{% endif %}
|
||||
{% if disabled %}disabled{% 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 }}</label>
|
||||
for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}">
|
||||
{{ setting }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
<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'] }}"
|
||||
<input id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
name="{{ setting }}"
|
||||
type="{{ setting_data['type'] }}"
|
||||
class="form-control plugin-setting"
|
||||
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
pattern="{{ setting_data['regex'] }}"
|
||||
value="{{ setting_value }}"
|
||||
data-default="{{ setting_data['default'] }}"
|
||||
data-original="{{ setting_value }}"
|
||||
data-default="{{ setting_default }}"
|
||||
{% if disabled %}disabled{% endif %}>
|
||||
<label for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}">{{ setting }}</label>
|
||||
<label for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ 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 %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
{% set blacklisted_settings = get_blacklisted_settings(current_endpoint == "global-config") %}
|
||||
<div class="card p-1 mb-4">
|
||||
<div class="card p-1 mb-4 sticky-card">
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}">
|
||||
<div class="d-flex flex-wrap justify-content-around align-items-center">
|
||||
<div class="dropdown btn-group">
|
||||
<button id="select-plugin"
|
||||
|
|
@ -75,166 +79,198 @@
|
|||
"title-class": " border-primary text-primary fw-bold shine"
|
||||
}
|
||||
} %}
|
||||
<div class="card tab-content position-relative">
|
||||
{% for plugin in plugins if get_filtered_settings(plugin["settings"], current_endpoint == "global-config") %}
|
||||
<div id="navs-plugins-{{ plugin['id'] }}"
|
||||
class="tab-pane fade{% if loop.index == 1 %} show active{% endif %}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="navs-plugins-{{ plugin['id'] }}-tab"
|
||||
data-type="{{ plugin['type'] }}">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
|
||||
{{ plugin["name"] }} - v{{ plugin["version"] }} - {{ plugin_types[plugin["type"]].get('icon', '<img src="' + pro_diamond_url + '"
|
||||
<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") %}
|
||||
{% if filtered_settings %}
|
||||
<div id="navs-plugins-{{ plugin['id'] }}"
|
||||
class="tab-pane fade{% if loop.index == 1 %} show active{% endif %}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="navs-plugins-{{ plugin['id'] }}-tab"
|
||||
data-type="{{ plugin['type'] }}">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
|
||||
{{ plugin["name"] }} - v{{ plugin["version"] }} - {{ plugin_types[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 mt-2">{{ plugin["description"] }}</p>
|
||||
</div>
|
||||
<div class="card-body row pb-0">
|
||||
{% for setting, setting_data in get_filtered_settings(plugin["settings"], current_endpoint == "global-config").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_config = config.get(setting, {}) %}
|
||||
{% set setting_value = setting_config.get("value", setting_data["default"]) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_template = setting_config.get("template", "") %}
|
||||
{% set disabled = setting_method not in ('ui', 'default') %}
|
||||
<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>
|
||||
<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 setting_template %}
|
||||
<span class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
|
||||
</h5>
|
||||
<p class="card-subtitle text-muted mt-2">{{ plugin["description"] }}</p>
|
||||
</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_config = config.get(setting, {}) %}
|
||||
{% set setting_value = setting_config.get("value", setting_data["default"]) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_default = setting_data.get("default", "") %}
|
||||
{% set setting_template = setting_config.get("template", "") %}
|
||||
{% set disabled = setting_method not in ('ui', 'default') %}
|
||||
<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>
|
||||
<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 setting_template %}
|
||||
<span 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="From template: {{ setting_template }}">
|
||||
<span class="bx bx-spreadsheet 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="From template: {{ setting_template }}">
|
||||
<span class="bx bx-spreadsheet bx-xs"></span>
|
||||
title="{{ setting_data['help']|capitalize }}">
|
||||
<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 %}
|
||||
{% if setting == "SERVER_NAME" and current_endpoint != "global-config" %}
|
||||
<input type="hidden" name="OLD_SERVER_NAME" value="{{ setting_value }}">
|
||||
{% 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 }}">
|
||||
<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>
|
||||
{% set multiples = get_multiples(get_filtered_settings(plugin["settings"], current_endpoint == "global-config")) %}
|
||||
{% if multiples %}
|
||||
{% set setting_id_prefix = "multiple-" %}
|
||||
{% set multiple_multiples = multiples|length > 1 %}
|
||||
<div class="card-body row card-body row border-primary border-top pt-3">
|
||||
<h5 class="row card-title text-uppercase">Multiple settings</h5>
|
||||
{% for multiple, settings in multiples.items() %}
|
||||
{% set multiple_settings = settings|length > 1 %}
|
||||
<div class="col-12{% if multiple_multiples %} col-md-6{% endif %}{% if multiples|length > 2 and not multiple_settings %} col-lg-4{% endif %} pb-3">
|
||||
<div class="row">
|
||||
<div class="ps-0 d-flex align-items-center justify-content-start">
|
||||
<h6 class="mb-0">{{ multiple.replace('-', ' ') | capitalize }}</h6>
|
||||
<button id="add-multiple-{{ plugin['id'] }}-{{ multiple }}"
|
||||
type="button"
|
||||
class="btn btn-xs btn-outline-bw-green rounded-pill add-multiple ms-2">
|
||||
<i class="bx bx-plus-circle bx-sm"></i> ADD
|
||||
</button>
|
||||
<button id="show-multiple-{{ plugin['id'] }}-{{ multiple }}"
|
||||
type="button"
|
||||
class="btn btn-xs btn-outline-secondary rounded-pill show-multiple ms-2"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#multiple-{{ plugin['id'] }}-{{ multiple }}"
|
||||
aria-expanded="true"
|
||||
aria-controls="multiple-{{ plugin['id'] }}-{{ multiple }}">
|
||||
<i class="bx bx-show-alt bx-sm"></i> HIDE
|
||||
</button>
|
||||
</div>
|
||||
<div id="multiple-{{ plugin['id'] }}-{{ multiple }}"
|
||||
class="collapse show multiple-collapse">
|
||||
<div class="row multiple-container mt-2 pt-2">
|
||||
{% for setting, setting_data in settings.items() if setting not in blacklisted_settings %}
|
||||
{% set setting_config = config.get(setting, {}) %}
|
||||
{% set setting_value = setting_config.get("value", setting_data["default"]) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_template = setting_config.get("template", "") %}
|
||||
{% set disabled = setting_method not in ('ui', 'default') %}
|
||||
<div id="multiple-{{ plugin['id'] }}-{{ multiple }}-{{ loop.index - 1 }}"
|
||||
class="col-12{% if multiple_multiples and multiple_settings %} col-md-6{% endif %}{% if not multiple_multiples %} col-lg-4{% endif %} ps-0 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'] }}"
|
||||
for="multiple-setting-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
class="form-label fw-semibold text-truncate">
|
||||
{{ setting_data["label"]|capitalize }}
|
||||
</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 setting_template %}
|
||||
<span 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="From template: {{ setting_template }}">
|
||||
<span class="bx bx-spreadsheet 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 }}">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% include "models/input_setting.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% set plugin_multiples = get_multiples(filtered_settings, config) %}
|
||||
{% if plugin_multiples %}
|
||||
{% set setting_id_prefix = "multiple-" %}
|
||||
{% 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', '') }}">
|
||||
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">
|
||||
{% for multiple, multiples in plugin_multiples.items() %}
|
||||
{% set multiple_settings = settings|length > 1 %}
|
||||
<div id="multiple-{{ plugin['id'] }}-{{ 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() %}
|
||||
<div class="row multiple-container pt-2 pb-2">
|
||||
{% set setting_id_suffix = "-" + setting_suffix %}
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h6 class="mb-0 me-2">
|
||||
{{ multiple.replace('-', ' ') | capitalize }}
|
||||
{% if setting_suffix != "0" %}#{{ setting_suffix }}{% endif %}
|
||||
</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
{% if setting_suffix == "0" %}
|
||||
<button id="add-multiple-{{ plugin['id'] }}-{{ 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> ADD
|
||||
</button>
|
||||
{% else %}
|
||||
<div>
|
||||
<button id="remove-multiple-{{ plugin['id'] }}-{{ 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> REMOVE
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button id="show-multiple-{{ plugin['id'] }}-{{ 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 }}"
|
||||
aria-expanded="true"
|
||||
aria-controls="multiple-{{ plugin['id'] }}-{{ multiple }}-{{ setting_suffix }}">
|
||||
<i class="bx bx-show-alt bx-sm"></i> HIDE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="multiple-{{ plugin['id'] }}-{{ 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 %}
|
||||
{% set setting_config = config.get(setting, {}) %}
|
||||
{% set setting_value = setting_config.get("value", setting_data["default"]) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_default = setting_data.get("default", "") %}
|
||||
{% set setting_template = setting_config.get("template", "") %}
|
||||
{% set disabled = setting_method not in ('ui', 'default') %}
|
||||
<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'] }}"
|
||||
for="multiple-setting-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
class="form-label fw-semibold text-truncate">
|
||||
{{ setting_data["label"]|capitalize }}
|
||||
</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 setting_template %}
|
||||
<span 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="From template: {{ setting_template }}">
|
||||
<span class="bx bx-spreadsheet 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 }}">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
<div class="form-floating mt-1">
|
||||
<select id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
<select id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
name="{{ setting }}"
|
||||
class="form-select"
|
||||
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}"
|
||||
data-default="{{ setting_data['default'] }}"
|
||||
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
data-original="{{ setting_value }}"
|
||||
data-default="{{ setting_default }}"
|
||||
{% if disabled %}disabled{% 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 }}</label>
|
||||
<label for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}">
|
||||
{{ setting }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,15 +43,36 @@ def handle_stop(signum, frame):
|
|||
stop(0, False)
|
||||
|
||||
|
||||
def get_multiples(settings: dict) -> Dict[str, dict]:
|
||||
multiples = {}
|
||||
def get_multiples(settings: dict, config: dict) -> Dict[str, Dict[str, Dict[str, dict]]]:
|
||||
plugin_multiples = {}
|
||||
for setting, data in settings.items():
|
||||
multiple = data.get("multiple")
|
||||
if multiple:
|
||||
if multiple not in multiples:
|
||||
multiples[multiple] = {}
|
||||
multiples[multiple].update({setting: data | {"setting_no_suffix": setting.rsplit("_", 1)[0] if match(r".+_\d+$", setting) else setting}})
|
||||
return multiples
|
||||
data = data | {"setting_no_suffix": setting}
|
||||
|
||||
if multiple not in plugin_multiples:
|
||||
plugin_multiples[multiple] = {}
|
||||
if "0" not in plugin_multiples[multiple]:
|
||||
plugin_multiples[multiple]["0"] = {}
|
||||
|
||||
plugin_multiples[multiple]["0"].update({setting: data})
|
||||
|
||||
for config_setting in config:
|
||||
setting_match = match(setting + r"_(?P<suffix>\d+)$", config_setting)
|
||||
if setting_match:
|
||||
suffix = setting_match.group("suffix")
|
||||
if suffix == "0":
|
||||
continue
|
||||
|
||||
if suffix not in plugin_multiples[multiple]:
|
||||
plugin_multiples[multiple][suffix] = {}
|
||||
plugin_multiples[multiple][suffix].update({config_setting: data})
|
||||
|
||||
# Sort the multiples and their settings
|
||||
for multiple, multiples in plugin_multiples.items():
|
||||
plugin_multiples[multiple] = dict(sorted(multiples.items()))
|
||||
|
||||
return plugin_multiples
|
||||
|
||||
|
||||
def get_filtered_settings(settings: dict, global_config: bool = False) -> Dict[str, dict]:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ from app.routes.global_config import global_config
|
|||
from app.routes.home import home
|
||||
from app.routes.instances import instances
|
||||
from app.routes.jobs import jobs
|
||||
from app.routes.modes import modes
|
||||
from app.routes.login import login
|
||||
from app.routes.logout import logout, logout_page
|
||||
from app.routes.logs import logs
|
||||
|
|
@ -406,8 +405,8 @@ def check_reloading():
|
|||
DATA["RELOADING"] = False
|
||||
|
||||
for f in DATA.get("TO_FLASH", []):
|
||||
if f["type"] == "error":
|
||||
flash(f["content"], "error")
|
||||
if f["type"] != "success":
|
||||
flash(f["content"], f["type"])
|
||||
else:
|
||||
flash(f["content"])
|
||||
|
||||
|
|
@ -419,6 +418,6 @@ def check_reloading():
|
|||
return jsonify({"reloading": DATA.get("RELOADING", False)})
|
||||
|
||||
|
||||
BLUEPRINTS = (bans, cache, configs, global_config, home, instances, jobs, modes, login, logout, logs, plugins, profile, reports, services, setup, totp)
|
||||
BLUEPRINTS = (bans, cache, configs, global_config, home, instances, jobs, login, logout, logs, plugins, profile, reports, services, setup, totp)
|
||||
for blueprint in BLUEPRINTS:
|
||||
app.register_blueprint(blueprint)
|
||||
|
|
|
|||
Loading…
Reference in a new issue