mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add configs and cache page to web UI
This commit is contained in:
parent
492b5b1944
commit
b75a0fe5f5
24 changed files with 1980 additions and 379 deletions
|
|
@ -1943,30 +1943,28 @@ class Database:
|
|||
|
||||
return config
|
||||
|
||||
def get_custom_configs(self, *, with_drafts: bool = False, as_dict: bool = False) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
||||
def get_custom_configs(self, *, with_drafts: bool = False, with_data: bool = True, as_dict: bool = False) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
||||
"""Get the custom configs from the database"""
|
||||
db_config = self.get_non_default_settings(with_drafts=with_drafts, filtered_settings={"USE_TEMPLATE"})
|
||||
|
||||
with self._db_session() as session:
|
||||
custom_configs = [
|
||||
{
|
||||
entities = [Custom_configs.service_id, Custom_configs.type, Custom_configs.name, Custom_configs.checksum, Custom_configs.method]
|
||||
if with_data:
|
||||
entities.append(Custom_configs.data)
|
||||
|
||||
custom_configs = []
|
||||
for custom_config in session.query(Custom_configs).with_entities(*entities):
|
||||
data = {
|
||||
"service_id": custom_config.service_id,
|
||||
"type": custom_config.type,
|
||||
"name": custom_config.name,
|
||||
"data": custom_config.data,
|
||||
"checksum": custom_config.checksum,
|
||||
"method": custom_config.method,
|
||||
"template": None,
|
||||
}
|
||||
for custom_config in (
|
||||
session.query(Custom_configs).with_entities(
|
||||
Custom_configs.service_id,
|
||||
Custom_configs.type,
|
||||
Custom_configs.name,
|
||||
Custom_configs.data,
|
||||
Custom_configs.method,
|
||||
)
|
||||
)
|
||||
]
|
||||
if with_data:
|
||||
data["data"] = custom_config.data
|
||||
custom_configs.append(data)
|
||||
|
||||
if not db_config:
|
||||
if as_dict:
|
||||
|
|
@ -1978,30 +1976,31 @@ class Database:
|
|||
return dict_custom_configs
|
||||
return custom_configs
|
||||
|
||||
template_entities = [Template_custom_configs.type, Template_custom_configs.name, Template_custom_configs.checksum]
|
||||
if with_data:
|
||||
template_entities.append(Template_custom_configs.data)
|
||||
|
||||
for service in session.query(Services).with_entities(Services.id).all():
|
||||
for key, value in db_config.items():
|
||||
if key.startswith(f"{service.id}_"):
|
||||
for template_config in (
|
||||
session.query(Template_custom_configs)
|
||||
.with_entities(Template_custom_configs.type, Template_custom_configs.name, Template_custom_configs.data)
|
||||
.filter_by(template_id=value)
|
||||
):
|
||||
for template_config in session.query(Template_custom_configs).with_entities(*template_entities).filter_by(template_id=value):
|
||||
if not any(
|
||||
custom_config["service_id"] == service.id
|
||||
and custom_config["type"] == template_config.type
|
||||
and custom_config["name"] == template_config.name
|
||||
for custom_config in custom_configs
|
||||
):
|
||||
custom_configs.append(
|
||||
{
|
||||
"service_id": service.id,
|
||||
"type": template_config.type,
|
||||
"name": template_config.name,
|
||||
"data": template_config.data,
|
||||
"method": "default",
|
||||
"template": value,
|
||||
}
|
||||
)
|
||||
custom_config = {
|
||||
"service_id": service.id,
|
||||
"type": template_config.type,
|
||||
"name": template_config.name,
|
||||
"checksum": template_config.checksum,
|
||||
"method": "default",
|
||||
"template": value,
|
||||
}
|
||||
if with_data:
|
||||
custom_config["data"] = template_config.data
|
||||
custom_configs.append(custom_config)
|
||||
|
||||
if as_dict:
|
||||
dict_custom_configs = {}
|
||||
|
|
@ -2012,6 +2011,102 @@ class Database:
|
|||
return dict_custom_configs
|
||||
return custom_configs
|
||||
|
||||
def get_custom_config(self, config_type: str, name: str, *, service_id: Optional[str] = None, with_data: bool = True) -> Dict[str, Any]:
|
||||
"""Get a custom config from the database"""
|
||||
with self._db_session() as session:
|
||||
entities = [Custom_configs.service_id, Custom_configs.type, Custom_configs.name, Custom_configs.checksum, Custom_configs.method]
|
||||
if with_data:
|
||||
entities.append(Custom_configs.data)
|
||||
|
||||
db_config = session.query(Custom_configs).with_entities(*entities).filter_by(service_id=service_id, type=config_type, name=name).first()
|
||||
|
||||
if not db_config:
|
||||
if service_id:
|
||||
service_config = self.get_non_default_settings(with_drafts=True, filtered_settings={"USE_TEMPLATE"})
|
||||
if service_config.get(f"{service_id}_USE_TEMPLATE"):
|
||||
with self._db_session() as session:
|
||||
template_config = (
|
||||
session.query(Template_custom_configs)
|
||||
.filter_by(template_id=service_config.get(f"{service_id}_USE_TEMPLATE"), type=config_type, name=name)
|
||||
.first()
|
||||
)
|
||||
if template_config:
|
||||
custom_config = {
|
||||
"service_id": service_id,
|
||||
"type": config_type,
|
||||
"name": name,
|
||||
"checksum": template_config.checksum,
|
||||
"method": "default",
|
||||
"template": service_config.get(f"{service_id}_USE_TEMPLATE"),
|
||||
}
|
||||
if with_data:
|
||||
custom_config["data"] = template_config.data
|
||||
return custom_config
|
||||
return {}
|
||||
|
||||
custom_config = {
|
||||
"service_id": service_id,
|
||||
"type": config_type,
|
||||
"name": name,
|
||||
"checksum": db_config.checksum,
|
||||
"method": db_config.method,
|
||||
"template": None,
|
||||
}
|
||||
if with_data:
|
||||
custom_config["data"] = db_config.data
|
||||
|
||||
return custom_config
|
||||
|
||||
def upsert_custom_config(self, config_type: str, name: str, config: Dict[str, Any], *, service_id: Optional[str] = None) -> str:
|
||||
"""Update or insert a custom config in the database"""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
filters = {
|
||||
"type": config_type,
|
||||
"name": name,
|
||||
}
|
||||
if service_id:
|
||||
filters["service_id"] = service_id
|
||||
|
||||
custom_config = session.query(Custom_configs).filter_by(**filters).first()
|
||||
|
||||
data = config["data"].encode("utf-8") if isinstance(config["data"], str) else config["data"]
|
||||
checksum = config.get("checksum", bytes_hash(data, algorithm="sha256"))
|
||||
|
||||
if not custom_config:
|
||||
session.add(
|
||||
Custom_configs(
|
||||
service_id=config.get("service_id"),
|
||||
type=config["type"],
|
||||
name=config["name"],
|
||||
data=data,
|
||||
checksum=checksum,
|
||||
method=config["method"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
custom_config.service_id = config.get("service_id")
|
||||
custom_config.data = data
|
||||
custom_config.checksum = checksum
|
||||
for key in ("type", "name", "method"):
|
||||
if key in config:
|
||||
setattr(custom_config, key, config[key])
|
||||
|
||||
with suppress(ProgrammingError, OperationalError):
|
||||
metadata = session.query(Metadata).get(1)
|
||||
if metadata is not None:
|
||||
metadata.custom_configs_changed = True
|
||||
metadata.last_custom_configs_change = datetime.now().astimezone()
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
return str(e)
|
||||
|
||||
return ""
|
||||
|
||||
def get_services_settings(self, methods: bool = False, with_drafts: bool = False) -> List[Dict[str, Any]]:
|
||||
"""Get the services' configs from the database"""
|
||||
services = []
|
||||
|
|
@ -3145,11 +3240,14 @@ class Database:
|
|||
ret_data["data"] = data.data
|
||||
return ret_data
|
||||
|
||||
def get_jobs_cache_files(self, *, job_name: str = "", plugin_id: str = "") -> List[Dict[str, Any]]:
|
||||
def get_jobs_cache_files(self, *, with_data: bool = True, job_name: str = "", plugin_id: str = "") -> List[Dict[str, Any]]:
|
||||
"""Get jobs cache files."""
|
||||
with self._db_session() as session:
|
||||
filters = {}
|
||||
query = session.query(Jobs_cache).with_entities(Jobs_cache.job_name, Jobs_cache.service_id, Jobs_cache.file_name, Jobs_cache.data)
|
||||
entities = [Jobs_cache.job_name, Jobs_cache.service_id, Jobs_cache.file_name, Jobs_cache.last_update, Jobs_cache.checksum]
|
||||
if with_data:
|
||||
entities.append(Jobs_cache.data)
|
||||
query = session.query(Jobs_cache).with_entities(*entities)
|
||||
|
||||
if job_name:
|
||||
query = query.filter_by(job_name=job_name)
|
||||
|
|
@ -3185,9 +3283,13 @@ class Database:
|
|||
"job_name": cache.job_name,
|
||||
"service_id": cache.service_id,
|
||||
"file_name": cache.file_name,
|
||||
"data": cache.data,
|
||||
"last_update": cache.last_update if cache.last_update is not None else "Never",
|
||||
"checksum": cache.checksum,
|
||||
}
|
||||
)
|
||||
if with_data:
|
||||
cache_files[-1]["data"] = cache.data
|
||||
|
||||
return cache_files
|
||||
|
||||
def add_instance(self, hostname: str, port: int, server_name: str, method: str, changed: Optional[bool] = True, *, name: Optional[str] = None) -> str:
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ class Config:
|
|||
elif setting not in config and plugins_settings[setting]["default"] == v:
|
||||
variables.pop(k)
|
||||
continue
|
||||
elif not new and setting != "IS_DRAFT" and config[setting]["method"] not in ("default", "ui"):
|
||||
elif not new and setting != "IS_DRAFT" and setting in config and config[setting]["method"] not in ("default", "ui"):
|
||||
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"})
|
||||
|
|
|
|||
|
|
@ -1,26 +1,47 @@
|
|||
from os.path import join, sep
|
||||
from io import BytesIO
|
||||
|
||||
from flask import Blueprint, render_template
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, send_file, url_for
|
||||
from flask_login import login_required
|
||||
from magic import Magic
|
||||
|
||||
from app.dependencies import BW_CONFIG, DB
|
||||
from app.utils import path_to_dict
|
||||
from app.dependencies import DB
|
||||
|
||||
|
||||
cache = Blueprint("cache", __name__)
|
||||
|
||||
SHOWN_FILE_TYPES = ("text/plain", "text/html", "text/css", "text/javascript", "application/json", "application/xml")
|
||||
|
||||
|
||||
@cache.route("/cache", methods=["GET"])
|
||||
@login_required
|
||||
def cache_page(): # TODO: refactor this function
|
||||
return render_template(
|
||||
"cache.html",
|
||||
folders=[
|
||||
path_to_dict(
|
||||
join(sep, "var", "cache", "bunkerweb"),
|
||||
is_cache=True,
|
||||
db_data=DB.get_jobs_cache_files(),
|
||||
services=BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME",)).get("SERVER_NAME", "").split(" "),
|
||||
)
|
||||
],
|
||||
def cache_page():
|
||||
return render_template("cache.html", caches=DB.get_jobs_cache_files(with_data=False))
|
||||
|
||||
|
||||
@cache.route("/cache/<string:service>/<string:plugin_id>/<string:job_name>/<string:file_name>", methods=["GET"])
|
||||
@login_required
|
||||
def cache_view(service: str, plugin_id: str, job_name: str, file_name: str):
|
||||
file_name = file_name.replace("_", "/")
|
||||
cache_file = DB.get_job_cache_file(
|
||||
job_name,
|
||||
file_name,
|
||||
service_id=service if service != "global" else None,
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
|
||||
download = request.args.get("download", False)
|
||||
if download:
|
||||
if not cache_file:
|
||||
return Response("Cache file not found", status=404)
|
||||
return send_file(BytesIO(cache_file), as_attachment=True, download_name=file_name)
|
||||
|
||||
if not cache_file:
|
||||
flash(f"Cache file {file_name} from job {job_name}, plugin {plugin_id}{', service ' + service if service != 'global' else ''} not found", "error")
|
||||
return redirect(url_for("cache.cache_page"))
|
||||
|
||||
file_type = Magic(mime=True).from_buffer(cache_file)
|
||||
|
||||
return render_template(
|
||||
"cache_view.html",
|
||||
cache_file=cache_file.decode("utf-8") if file_type in SHOWN_FILE_TYPES else f"File is of type {file_type}, Download it to view the content",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,132 +1,338 @@
|
|||
from copy import deepcopy
|
||||
from os.path import join, sep
|
||||
from json import JSONDecodeError, loads
|
||||
from re import match
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Dict, Literal, Optional
|
||||
|
||||
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 # 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
|
||||
from app.routes.utils import handle_error, verify_data_in_form, wait_applying
|
||||
|
||||
|
||||
configs = Blueprint("configs", __name__)
|
||||
|
||||
CONFIG_TYPES = {
|
||||
"HTTP": {"context": "global", "description": "Configurations at the HTTP level of NGINX."},
|
||||
"SERVER_HTTP": {"context": "multisite", "description": "Configurations at the HTTP/Server level of NGINX."},
|
||||
"DEFAULT_SERVER_HTTP": {
|
||||
"context": "global",
|
||||
"description": 'Configurations at the Server level of NGINX, specifically for the "default server" when the supplied client name doesn\'t match any server name in SERVER_NAME.',
|
||||
},
|
||||
"MODSEC_CRS": {"context": "multisite", "description": "Configurations applied before the OWASP Core Rule Set is loaded."},
|
||||
"MODSEC": {
|
||||
"context": "multisite",
|
||||
"description": "Configurations applied after the OWASP Core Rule Set is loaded, or used when the Core Rule Set is not loaded.",
|
||||
},
|
||||
"STREAM": {"context": "global", "description": "Configurations at the Stream level of NGINX."},
|
||||
"SERVER_STREAM": {"context": "multisite", "description": "Configurations at the Stream/Server level of NGINX."},
|
||||
"CRS_PLUGINS_BEFORE": {"context": "multisite", "description": "Configurations applied before the OWASP Core Rule Set plugins are loaded."},
|
||||
"CRS_PLUGINS_AFTER": {"context": "multisite", "description": "Configurations applied after the OWASP Core Rule Set plugins are loaded."},
|
||||
}
|
||||
|
||||
@configs.route("/configs", methods=["GET", "POST"])
|
||||
|
||||
@configs.route("/configs", methods=["GET"])
|
||||
@login_required
|
||||
def configs_page(): # TODO: refactor this function
|
||||
db_configs = DB.get_custom_configs()
|
||||
def configs_page():
|
||||
return render_template("configs.html", configs=DB.get_custom_configs(with_drafts=True, with_data=False))
|
||||
|
||||
|
||||
@configs.route("/configs/delete", methods=["POST"])
|
||||
@login_required
|
||||
def configs_delete():
|
||||
verify_data_in_form(
|
||||
data={"configs": None},
|
||||
err_message="Missing configs parameter on /configs/delete.",
|
||||
redirect_url="configs",
|
||||
next=True,
|
||||
)
|
||||
configs = request.form["configs"]
|
||||
if not configs:
|
||||
return handle_error("No configs selected.", "configs", True)
|
||||
try:
|
||||
configs = loads(configs)
|
||||
except JSONDecodeError:
|
||||
return handle_error("Invalid configs parameter on /configs/delete.", "configs", True)
|
||||
DATA.load_from_file()
|
||||
|
||||
def delete_configs(configs: Dict[str, str]):
|
||||
wait_applying()
|
||||
|
||||
db_configs = DB.get_custom_configs(with_drafts=True)
|
||||
configs_to_delete = set()
|
||||
non_ui_configs = set()
|
||||
|
||||
for i, db_config in enumerate(db_configs):
|
||||
for config in configs:
|
||||
if db_config["name"] == config["name"] and db_config["service_id"] == config["service"] and db_config["type"] == config["type"]:
|
||||
key = f"{(config['service'] + '/') if config['service'] else ''}{config['type']}/{config['name']}"
|
||||
if db_config["method"] != "ui":
|
||||
non_ui_configs.add(key)
|
||||
continue
|
||||
configs_to_delete.add(key)
|
||||
del db_configs[i]
|
||||
|
||||
for non_ui_config in non_ui_configs:
|
||||
DATA["TO_FLASH"].append(
|
||||
{
|
||||
"content": f"Custom config {non_ui_config} is not a UI custom config and will not be deleted.",
|
||||
"type": "error",
|
||||
}
|
||||
)
|
||||
|
||||
if not configs_to_delete:
|
||||
DATA["TO_FLASH"].append({"content": "All selected custom configs could not be found or are not UI custom configs.", "type": "error"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
error = DB.save_custom_configs(db_configs, "ui")
|
||||
if error:
|
||||
DATA["TO_FLASH"].append({"content": f"An error occurred while saving the custom configs: {error}", "type": "error"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
DATA["TO_FLASH"].append({"content": f"Deleted config{'s' if len(configs_to_delete) > 1 else ''}: {', '.join(configs_to_delete)}", "type": "success"})
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
Thread(target=delete_configs, args=(configs,)).start()
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"loading",
|
||||
next=url_for("configs.configs_page"),
|
||||
message=f"Deleting selected config{'s' if len(configs) > 1 else ''}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@configs.route("/configs/new", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def configs_new():
|
||||
if request.method == "POST":
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "configs")
|
||||
|
||||
operation = ""
|
||||
|
||||
verify_data_in_form(
|
||||
data={"operation": ("new", "edit", "delete"), "type": "file", "path": None},
|
||||
err_message="Invalid operation parameter on /configs.",
|
||||
redirect_url="configs",
|
||||
data={"service": None},
|
||||
err_message="Missing service parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
service = request.form["service"]
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if service != "no service" and service not in services:
|
||||
return handle_error(f"Service {service} does not exist.", "configs/new", True)
|
||||
|
||||
# Check variables
|
||||
variables = deepcopy(request.form.to_dict())
|
||||
del variables["csrf_token"]
|
||||
verify_data_in_form(
|
||||
data={"type": None},
|
||||
err_message="Missing type parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
config_type = request.form["type"]
|
||||
if config_type not in CONFIG_TYPES:
|
||||
return handle_error("Invalid type parameter on /configs/new.", "configs/new", True)
|
||||
|
||||
if variables["type"] != "file":
|
||||
return handle_error("Invalid type parameter on /configs.", "configs", True)
|
||||
verify_data_in_form(
|
||||
data={"name": None},
|
||||
err_message="Missing name parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
config_name = request.form["name"]
|
||||
if not match(r"^[\w_-]{1,64}$", config_name):
|
||||
return handle_error("Invalid name parameter on /configs/new.", "configs/new", True)
|
||||
|
||||
# TODO: revamp this to use a path but a form to edit the content
|
||||
verify_data_in_form(
|
||||
data={"value": None},
|
||||
err_message="Missing value parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
config_value = request.form["value"].replace("\r\n", "\n").strip()
|
||||
DATA.load_from_file()
|
||||
|
||||
# operation = BW_CUSTOM_CONFIGS.check_path(variables["path"])
|
||||
def create_config(
|
||||
service: Optional[str],
|
||||
config_type: Literal[
|
||||
"HTTP", "SERVER_HTTP", "DEFAULT_SERVER_HTTP", "MODSEC_CRS", "MODSEC", "STREAM", "SERVER_STREAM", "CRS_PLUGINS_BEFORE", "CRS_PLUGINS_AFTER"
|
||||
],
|
||||
config_name: str,
|
||||
config_value: str,
|
||||
):
|
||||
wait_applying()
|
||||
config_type = config_type.lower()
|
||||
|
||||
# if operation:
|
||||
# return handle_error(operation, "configs", True)
|
||||
db_configs = DB.get_custom_configs(with_drafts=True)
|
||||
for i, db_config in enumerate(db_configs.copy()):
|
||||
if db_config["method"] == "default" and db_config["template"]:
|
||||
del db_configs[i]
|
||||
continue
|
||||
|
||||
old_name = variables.get("old_name", "").replace(".conf", "")
|
||||
name = variables.get("name", old_name).replace(".conf", "")
|
||||
path_exploded = variables["path"].split(sep)
|
||||
service_id = (path_exploded[5] if len(path_exploded) > 6 else None) or None
|
||||
root_dir = path_exploded[4].replace("-", "_").lower()
|
||||
|
||||
if not old_name and not name:
|
||||
return handle_error("Missing name parameter on /configs.", "configs", True)
|
||||
|
||||
index = -1
|
||||
for i, db_config in enumerate(db_configs):
|
||||
if db_config["type"] == root_dir and db_config["name"] == name and db_config["service_id"] == service_id:
|
||||
if request.form["operation"] == "new":
|
||||
return handle_error(f"Config {name} already exists{f' for service {service_id}' if service_id else ''}", "configs", True)
|
||||
elif db_config["method"] not in ("ui", "manual"):
|
||||
return handle_error(
|
||||
f"Can't edit config {name}{f' for service {service_id}' if service_id else ''} because it was not created by the UI or manually",
|
||||
"configs",
|
||||
True,
|
||||
if db_config["type"] == config_type and db_config["name"] == config_name and db_config["service_id"] == service:
|
||||
DATA["TO_FLASH"].append(
|
||||
{
|
||||
"content": f"Config {config_type}/{config_name}{' for service ' + service if service else ''} already exists",
|
||||
"type": "error",
|
||||
}
|
||||
)
|
||||
index = i
|
||||
break
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
# New or edit a config
|
||||
if request.form["operation"] in ("new", "edit"):
|
||||
if not PLUGIN_NAME_RX.match(name):
|
||||
return handle_error(
|
||||
f"Invalid {variables['type']} name. (Can only contain numbers, letters, underscores, dots and hyphens (min 4 characters and max 64))",
|
||||
"configs",
|
||||
True,
|
||||
)
|
||||
db_configs[i] = {
|
||||
"service_id": db_config["service_id"],
|
||||
"type": db_config["type"],
|
||||
"name": db_config["name"],
|
||||
"data": db_config["data"],
|
||||
"method": db_config["method"],
|
||||
}
|
||||
if "checksum" in db_config:
|
||||
db_configs[i]["checksum"] = db_config["checksum"]
|
||||
|
||||
content = BeautifulSoup(variables["content"], "html.parser").get_text()
|
||||
new_config = {
|
||||
"type": config_type,
|
||||
"name": config_name,
|
||||
"data": config_value,
|
||||
"method": "ui",
|
||||
}
|
||||
if service != "no service":
|
||||
new_config["service_id"] = service
|
||||
db_configs.append(new_config)
|
||||
|
||||
if request.form["operation"] == "new":
|
||||
db_configs.append({"type": root_dir, "name": name, "service_id": service_id, "data": content, "method": "ui"})
|
||||
operation = f"Created config {name}{f' for service {service_id}' if service_id else ''}"
|
||||
elif request.form["operation"] == "edit":
|
||||
if index == -1:
|
||||
return handle_error(
|
||||
f"Can't edit config {name}{f' for service {service_id}' if service_id else ''} because it doesn't exist", "configs", True
|
||||
)
|
||||
error = DB.save_custom_configs(db_configs, "ui")
|
||||
if error:
|
||||
DATA["TO_FLASH"].append({"content": f"An error occurred while saving the custom configs: {error}", "type": "error"})
|
||||
return
|
||||
DATA["TO_FLASH"].append(
|
||||
{
|
||||
"content": f"Created custom configuration {config_type}/{config_name}{' for service ' + service if service else ''}",
|
||||
"type": "success",
|
||||
}
|
||||
)
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
if old_name != name:
|
||||
db_configs[index]["name"] = name
|
||||
elif db_configs[index]["data"] == content:
|
||||
return handle_error(
|
||||
f"Config {name} was not edited because no values were changed{f' for service {service_id}' if service_id else ''}",
|
||||
"configs",
|
||||
True,
|
||||
)
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
Thread(target=create_config, args=(service if service != "no service" else None, config_type, config_name, config_value)).start()
|
||||
|
||||
db_configs[index]["data"] = content
|
||||
operation = f"Edited config {name}{f' for service {service_id}' if service_id else ''}"
|
||||
return redirect(
|
||||
url_for(
|
||||
"loading",
|
||||
next=url_for(
|
||||
"configs.configs_edit", service="global" if service == "no service" else service, config_type=config_type.lower(), name=config_name
|
||||
),
|
||||
message=f"Creating custom configuration {config_type}/{config_name}{' for service' + service if service != 'no service' else ''}",
|
||||
)
|
||||
)
|
||||
|
||||
# Delete a config
|
||||
elif request.form["operation"] == "delete":
|
||||
if index == -1:
|
||||
return handle_error(f"Can't delete config {name}{f' for service {service_id}' if service_id else ''} because it doesn't exist", "configs", True)
|
||||
clone = request.args.get("clone", "")
|
||||
config_service = ""
|
||||
config_type = ""
|
||||
config_name = ""
|
||||
|
||||
del db_configs[index]
|
||||
operation = f"Deleted config {name}{f' for service {service_id}' if service_id else ''}"
|
||||
if clone:
|
||||
config_service, config_type, config_name = clone.split("/")
|
||||
db_custom_config = DB.get_custom_config(config_type, config_name, service_id=config_service if config_service != "global" else None, with_data=True)
|
||||
clone = db_custom_config.get("data", b"").decode("utf-8")
|
||||
return render_template(
|
||||
"config_edit.html", config_types=CONFIG_TYPES, config_value=clone, config_service=config_service, type=config_type.upper(), name=config_name
|
||||
)
|
||||
|
||||
error = DB.save_custom_configs([config for config in db_configs if config["method"] == "ui"], "ui")
|
||||
|
||||
@configs.route("/configs/<string:service>/<string:config_type>/<string:name>", methods=["GET", "POST"]) # TODO: finish saving
|
||||
@login_required
|
||||
def configs_edit(service: str, config_type: str, name: str):
|
||||
if service == "global":
|
||||
service = None
|
||||
|
||||
db_config = DB.get_custom_config(config_type, name, service_id=service, with_data=True)
|
||||
if not db_config:
|
||||
return handle_error(f"Config {config_type}/{name}{' for service ' + service if service else ''} does not exist.", "configs", True)
|
||||
|
||||
if request.method == "POST":
|
||||
if not db_config["template"] and db_config["method"] != "ui":
|
||||
return handle_error(
|
||||
f"Config {config_type}/{name}{' for service ' + service if service else ''} is not a UI custom config and cannot be edited.", "configs", True
|
||||
)
|
||||
|
||||
verify_data_in_form(
|
||||
data={"service": None},
|
||||
err_message="Missing service parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
new_service = request.form["service"]
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if new_service != "no service" and new_service not in services:
|
||||
return handle_error(f"Service {new_service} does not exist.", "configs/new", True)
|
||||
|
||||
if new_service == "no service":
|
||||
new_service = None
|
||||
|
||||
verify_data_in_form(
|
||||
data={"type": None},
|
||||
err_message="Missing type parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
new_type = request.form["type"]
|
||||
if new_type not in CONFIG_TYPES:
|
||||
return handle_error("Invalid type parameter on /configs/new.", "configs/new", True)
|
||||
new_type = new_type.lower()
|
||||
|
||||
verify_data_in_form(
|
||||
data={"name": None},
|
||||
err_message="Missing name parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
new_name = request.form["name"]
|
||||
if not match(r"^[\w_-]{1,64}$", new_name):
|
||||
return handle_error("Invalid name parameter on /configs/new.", "configs/new", True)
|
||||
|
||||
verify_data_in_form(
|
||||
data={"value": None},
|
||||
err_message="Missing value parameter on /configs/new.",
|
||||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
config_value = request.form["value"].replace("\r\n", "\n").strip()
|
||||
DATA.load_from_file()
|
||||
|
||||
if config_value == db_config["data"].decode("utf-8").strip():
|
||||
return handle_error("No values were changed.", "configs", True)
|
||||
|
||||
error = DB.upsert_custom_config(
|
||||
config_type,
|
||||
name,
|
||||
{
|
||||
"service_id": service,
|
||||
"type": new_type,
|
||||
"name": new_name,
|
||||
"data": config_value,
|
||||
"method": "ui",
|
||||
},
|
||||
service_id=new_service,
|
||||
)
|
||||
if error:
|
||||
LOGGER.error(f"Could not save custom configs: {error}")
|
||||
return handle_error("Couldn't save custom configs", "configs", True)
|
||||
flash(f"An error occurred while saving the custom configs: {error}", "error")
|
||||
else:
|
||||
flash(f"Saved custom configuration {new_type}/{new_name}{' for service ' + new_service if new_service else ''}", "success")
|
||||
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
flash(operation)
|
||||
|
||||
return redirect(url_for("loading", next=url_for("configs.configs_page"), message="Update configs"))
|
||||
return redirect(
|
||||
url_for(
|
||||
"configs.configs_edit",
|
||||
service=new_service,
|
||||
config_type=new_type,
|
||||
name=new_name,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"configs.html",
|
||||
folders=[
|
||||
path_to_dict(
|
||||
join(sep, "etc", "bunkerweb", "configs"),
|
||||
db_data=db_configs,
|
||||
services=BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME",)).get("SERVER_NAME", "").split(" "),
|
||||
)
|
||||
],
|
||||
"config_edit.html",
|
||||
config_types=CONFIG_TYPES,
|
||||
config_value=db_config["data"].decode("utf-8"),
|
||||
config_service=db_config.get("service_id"),
|
||||
type=db_config["type"].upper(),
|
||||
name=db_config["name"],
|
||||
config_method=db_config["method"],
|
||||
config_template=db_config.get("template"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,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)
|
||||
return handle_error(f"No instance{'s' if len(instances) > 1 else ''} selected.", "instances", True)
|
||||
DATA.load_from_file()
|
||||
|
||||
if action == "ping":
|
||||
|
|
@ -114,12 +114,16 @@ def instances_action(action: Literal["ping", "reload", "stop", "delete"]): # TO
|
|||
flash(f"Instance {non_ui_instance} is not a UI instance and will not be deleted.", "error")
|
||||
|
||||
if not delete_instances:
|
||||
return handle_error("All selected instances could not be found or are not UI instances.", "instances", True)
|
||||
return handle_error(
|
||||
f"{'All selected instances' if len(delete_instances) > 1 else 'Selected instance'} could not be found or {'are not UI instances' if len(delete_instances) > 1 else 'is not an UI instance'}.",
|
||||
"instances",
|
||||
True,
|
||||
)
|
||||
|
||||
ret = DB.delete_instances(delete_instances)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't delete the instances in the database: {ret}", "instances", True)
|
||||
flash(f"Instances {', '.join(delete_instances)} deleted successfully.", "success")
|
||||
return handle_error(f"Couldn't delete the instance{'s' if len(delete_instances) > 1 else ''} in the database: {ret}", "instances", True)
|
||||
flash(f"Instance{'s' if len(delete_instances) > 1 else ''} {', '.join(delete_instances)} deleted successfully.", "success")
|
||||
else:
|
||||
|
||||
def execute_action(instance):
|
||||
|
|
@ -152,6 +156,6 @@ def instances_action(action: Literal["ping", "reload", "stop", "delete"]): # TO
|
|||
url_for(
|
||||
"loading",
|
||||
next=url_for("instances.instances_page"),
|
||||
message=(f"{ACTIONS[action]['present']} instances {', '.join(instances)}"),
|
||||
message=(f"{ACTIONS[action]['present']} instance{'s' if len(instances) > 1 else ''} {', '.join(instances)}"),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -151,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 service{'s' if len(services_to_delete) > 1 else ''}: {', '.join(services_to_delete)}", "type": "success"})
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
|
|
@ -206,7 +206,7 @@ def services_service_page(service: str):
|
|||
|
||||
if mode == "easy":
|
||||
db_templates = DB.get_templates()
|
||||
db_custom_configs = DB.get_custom_configs(as_dict=True)
|
||||
db_custom_configs = DB.get_custom_configs(with_drafts=True, as_dict=True)
|
||||
|
||||
for variable, value in variables.copy().items():
|
||||
conf_match = CUSTOM_CONF_RX.match(variable)
|
||||
|
|
@ -343,7 +343,7 @@ def services_service_page(service: str):
|
|||
search_type = request.args.get("type", "all")
|
||||
template = request.args.get("template", "high")
|
||||
db_templates = DB.get_templates()
|
||||
db_custom_configs = DB.get_custom_configs(as_dict=True)
|
||||
db_custom_configs = DB.get_custom_configs(with_drafts=True, as_dict=True)
|
||||
clone = None
|
||||
if service == "new":
|
||||
clone = request.args.get("clone", "")
|
||||
|
|
|
|||
|
|
@ -609,3 +609,18 @@ a.badge:hover {
|
|||
color: #6a9955;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
table.table.dataTable > tbody > tr.selected a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
table.table.dataTable > tbody > tr.selected .btn-outline-secondary {
|
||||
color: #fff !important;
|
||||
border-color: #fff !important;
|
||||
}
|
||||
|
||||
table.table.dataTable > tbody > tr.selected .btn-outline-secondary:hover {
|
||||
color: black !important;
|
||||
background-color: #fff !important;
|
||||
border-color: #fff !important;
|
||||
}
|
||||
|
|
|
|||
133
src/ui/app/static/js/pages/cache.js
Normal file
133
src/ui/app/static/js/pages/cache.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
$(document).ready(function () {
|
||||
const cacheNumber = parseInt($("#cache_number").val());
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
};
|
||||
|
||||
if (cacheNumber > 10) {
|
||||
layout.topStart.pageLength = {
|
||||
menu: [10, 25, 50, 100, { label: "All", value: -1 }],
|
||||
};
|
||||
layout.bottomEnd.paging = true;
|
||||
}
|
||||
|
||||
layout.topStart.buttons = [
|
||||
{
|
||||
extend: "colvis",
|
||||
columns: "th:not(:first-child):not(:last-child)",
|
||||
text: '<span class="tf-icons bx bx-columns bx-18px me-2"></span>Columns',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
columnText: function (dt, idx, title) {
|
||||
return idx + 1 + ". " + title;
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "colvisRestore",
|
||||
text: '<span class="tf-icons bx bx-reset bx-18px me-2"></span>Reset<span class="d-none d-md-inline"> columns</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
},
|
||||
{
|
||||
extend: "collection",
|
||||
text: '<span class="tf-icons bx bx-export bx-18px me-2"></span>Export',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
buttons: [
|
||||
{
|
||||
extend: "copy",
|
||||
text: '<span class="tf-icons bx bx-copy bx-18px me-2"></span>Copy current page',
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
page: "current",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "csv",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>CSV',
|
||||
bom: true,
|
||||
filename: "bw_cache",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "excel",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>Excel',
|
||||
filename: "bw_cache",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const cache_table = new DataTable("#cache", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("text-center align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[2, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ cache files",
|
||||
infoEmpty: "No cache files available",
|
||||
infoFiltered: "(filtered from _MAX_ total cache files)",
|
||||
lengthMenu: "Display _MENU_ cache files",
|
||||
zeroRecords: "No matching cache files found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d cache files",
|
||||
0: "No cache files selected",
|
||||
1: "Selected 1 cache file",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#cache_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
});
|
||||
|
||||
cache_table.on("mouseenter", "td", function () {
|
||||
const rowIdx = cache_table.cell(this).index().row;
|
||||
|
||||
cache_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
cache_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (cache_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
cache_table.on("mouseleave", "td", function () {
|
||||
cache_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
});
|
||||
21
src/ui/app/static/js/pages/cache_view.js
Normal file
21
src/ui/app/static/js/pages/cache_view.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
$(document).ready(function () {
|
||||
const editorElement = $("#cache-value");
|
||||
const initialContent = editorElement.text().trim();
|
||||
const editor = ace.edit(editorElement[0]);
|
||||
editor.setTheme("ace/theme/cloud9_day"); // cloud9_night when dark mode is supported
|
||||
editor.session.setMode("ace/mode/text");
|
||||
editor.setReadOnly(true);
|
||||
|
||||
// Set the editor's initial content
|
||||
editor.setValue(initialContent, -1); // The second parameter moves the cursor to the start
|
||||
|
||||
editor.setOptions({
|
||||
fontSize: "14px",
|
||||
showPrintMargin: false,
|
||||
tabSize: 2,
|
||||
useSoftTabs: true,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
});
|
||||
248
src/ui/app/static/js/pages/config_edit.js
Normal file
248
src/ui/app/static/js/pages/config_edit.js
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
$(document).ready(function () {
|
||||
let selectedService = $("#selected-service").val().trim();
|
||||
const originalService = selectedService;
|
||||
let selectedType = $("#selected-type").val().trim();
|
||||
const originalType = selectedType;
|
||||
const originalName = $("#config-name").val().trim();
|
||||
const editorElement = $("#config-value");
|
||||
const initialContent = editorElement.text().trim();
|
||||
const editor = ace.edit(editorElement[0]);
|
||||
editor.setTheme("ace/theme/cloud9_day"); // cloud9_night when dark mode is supported
|
||||
|
||||
const language = editorElement.data("language"); // TODO: Support ModSecurity
|
||||
if (language === "NGINX") {
|
||||
editor.session.setMode("ace/mode/nginx");
|
||||
} else {
|
||||
editor.session.setMode("ace/mode/text"); // Default mode if language is unrecognized
|
||||
}
|
||||
|
||||
const method = editorElement.data("method");
|
||||
const template = editorElement.data("template");
|
||||
if (method !== "ui" && template === "") {
|
||||
editor.setReadOnly(true);
|
||||
}
|
||||
|
||||
// Set the editor's initial content
|
||||
editor.setValue(initialContent, -1); // The second parameter moves the cursor to the start
|
||||
|
||||
editor.setOptions({
|
||||
fontSize: "14px",
|
||||
showPrintMargin: false,
|
||||
tabSize: 2,
|
||||
useSoftTabs: true,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
|
||||
const $serviceSearch = $("#service-search");
|
||||
const $serviceDropdownMenu = $("#services-dropdown-menu");
|
||||
const $serviceDropdownItems = $("#services-dropdown-menu li.nav-item");
|
||||
const $typeDropdownItems = $("#types-dropdown-menu li.nav-item");
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const changeTypesVisibility = () => {
|
||||
$typeDropdownItems.each(function () {
|
||||
const item = $(this);
|
||||
item.toggle(
|
||||
selectedService === "no service" ||
|
||||
item.data("context") === "multisite",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
$("#select-service").on("click", () => $serviceSearch.focus());
|
||||
|
||||
$serviceSearch.on(
|
||||
"input",
|
||||
debounce((e) => {
|
||||
const inputValue = e.target.value.toLowerCase();
|
||||
let visibleItems = 0;
|
||||
|
||||
$serviceDropdownItems.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 ($serviceDropdownMenu.find(".no-service-items").length === 0) {
|
||||
$serviceDropdownMenu.append(
|
||||
'<li class="no-service-items dropdown-item text-muted">No Item</li>',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$serviceDropdownMenu.find(".no-service-items").remove();
|
||||
}
|
||||
}, 50),
|
||||
);
|
||||
|
||||
$(document).on("hidden.bs.dropdown", "#select-service", function () {
|
||||
$("#service-search").val("").trigger("input");
|
||||
});
|
||||
|
||||
$serviceDropdownItems.on("click", function () {
|
||||
selectedService = $(this).text().trim();
|
||||
changeTypesVisibility();
|
||||
if (
|
||||
selectedService !== "no service" &&
|
||||
$(`#config-type-${selectedType}`).data("context") !== "multisite"
|
||||
) {
|
||||
const firstMultisiteType = $(
|
||||
`#types-dropdown-menu li.nav-item[data-context="multisite"]`,
|
||||
).first();
|
||||
$("#select-type")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
`Switched to ${firstMultisiteType
|
||||
.text()
|
||||
.trim()} as ${selectedType} is not a valid multisite type.`,
|
||||
)
|
||||
.tooltip("show");
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
$("#select-type")
|
||||
.parent()
|
||||
.tooltip("hide")
|
||||
.attr("data-bs-original-title", "");
|
||||
}, 2000);
|
||||
|
||||
firstMultisiteType.find("button").tab("show");
|
||||
}
|
||||
});
|
||||
|
||||
$typeDropdownItems.on("click", function () {
|
||||
selectedType = $(this).text().trim();
|
||||
if (selectedType.startsWith("CRS") || selectedType.startsWith("MODSEC")) {
|
||||
editor.session.setMode("ace/mode/text"); // TODO: Support ModSecurity
|
||||
} else {
|
||||
editor.session.setMode("ace/mode/nginx");
|
||||
}
|
||||
});
|
||||
|
||||
$(".save-config").on("click", function () {
|
||||
const value = editor.getValue().trim();
|
||||
if (
|
||||
value &&
|
||||
value === initialContent &&
|
||||
selectedService === originalService &&
|
||||
selectedType === originalType &&
|
||||
$("#config-name").val().trim() === originalName
|
||||
) {
|
||||
alert("No changes detected.");
|
||||
return;
|
||||
}
|
||||
|
||||
const $configInput = $("#config-name");
|
||||
const configName = $configInput.val().trim();
|
||||
const pattern = $configInput.attr("pattern");
|
||||
let errorMessage = "";
|
||||
let isValid = true;
|
||||
|
||||
if (!configName) {
|
||||
errorMessage = "A custom configuration name is required.";
|
||||
isValid = false;
|
||||
} else if (pattern && !new RegExp(pattern).test(configName))
|
||||
isValid = false;
|
||||
|
||||
if (!isValid) {
|
||||
$configInput
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
errorMessage || "Please enter a valid configuration name.",
|
||||
)
|
||||
.tooltip("show");
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
$configInput.tooltip("hide").attr("data-bs-original-title", "");
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Saving configuration...");
|
||||
console.log("Service:", selectedService);
|
||||
console.log("Type:", selectedType);
|
||||
console.log("Name:", configName);
|
||||
console.log("Value:", value);
|
||||
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: window.location.href,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "service",
|
||||
value: $("<div>").text(selectedService).html(),
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "type",
|
||||
value: $("<div>").text(selectedType).html(),
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "name",
|
||||
value: $("<div>").text(configName).html(),
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "value",
|
||||
value: $("<div>").text(value).html(),
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("<div>").text($("#csrf_token").val()).html(), // Sanitize the value
|
||||
}),
|
||||
);
|
||||
|
||||
$(window).off("beforeunload");
|
||||
form.appendTo("body").submit();
|
||||
});
|
||||
|
||||
changeTypesVisibility();
|
||||
|
||||
$(window).on("beforeunload", function (e) {
|
||||
const value = editor.getValue().trim();
|
||||
if (
|
||||
value &&
|
||||
value === initialContent &&
|
||||
selectedService === originalService &&
|
||||
selectedType === originalType &&
|
||||
$("#config-name").val().trim() === originalName
|
||||
)
|
||||
return;
|
||||
|
||||
// Cross-browser compatibility (for older browsers)
|
||||
var message =
|
||||
"Are you sure you want to leave? Changes you made may not be saved.";
|
||||
e.returnValue = message; // Standard for most browsers
|
||||
return message; // Required for some browsers
|
||||
});
|
||||
});
|
||||
316
src/ui/app/static/js/pages/configs.js
Normal file
316
src/ui/app/static/js/pages/configs.js
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
$(document).ready(function () {
|
||||
var actionLock = false;
|
||||
const configNumber = parseInt($("#configs_number").val());
|
||||
|
||||
const setupDeletionModal = (configs) => {
|
||||
const delete_modal = $("#modal-delete-configs");
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Name</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="fw-bold">Type</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="fw-bold">Service</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-configs-delete").append(list);
|
||||
|
||||
configs.forEach((config) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item align-items-center text-center" style="flex: 1 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${config.name}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
list.append(listItem);
|
||||
|
||||
const id = `${config.type.toLowerCase()}-${config.service.replaceAll(
|
||||
".",
|
||||
"_",
|
||||
)}-${config.name}`;
|
||||
|
||||
// Clone the type element and append it to the list item
|
||||
const typeClone = $(`#type-${id}`).clone();
|
||||
const typeListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 1 0;"></li>`,
|
||||
);
|
||||
typeListItem.append(typeClone.removeClass("highlight"));
|
||||
list.append(typeListItem);
|
||||
|
||||
// Clone the service element and append it to the list item
|
||||
const serviceClone = $(`#service-${id}`).clone();
|
||||
const serviceListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 1 0;"></li>`,
|
||||
);
|
||||
serviceListItem.append(serviceClone.removeClass("highlight"));
|
||||
list.append(serviceListItem);
|
||||
serviceClone.find('[data-bs-toggle="tooltip"]').tooltip();
|
||||
|
||||
$("#selected-configs-delete").append(list);
|
||||
});
|
||||
|
||||
const modal = new bootstrap.Modal(delete_modal);
|
||||
delete_modal
|
||||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to delete the selected custom configuration${"s".repeat(
|
||||
configs.length > 1,
|
||||
)}?`,
|
||||
);
|
||||
modal.show();
|
||||
|
||||
configs.forEach((config) => {
|
||||
if (config.service === "global") {
|
||||
config.service = null;
|
||||
}
|
||||
});
|
||||
$("#selected-configs-input-delete").val(JSON.stringify(configs));
|
||||
};
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
};
|
||||
|
||||
if (configNumber > 10) {
|
||||
layout.topStart.pageLength = {
|
||||
menu: [10, 25, 50, 100, { label: "All", value: -1 }],
|
||||
};
|
||||
layout.bottomEnd.paging = true;
|
||||
}
|
||||
|
||||
layout.topStart.buttons = [
|
||||
{
|
||||
extend: "colvis",
|
||||
columns: "th:not(:first-child):not(:nth-child(2)):not(:last-child)",
|
||||
text: '<span class="tf-icons bx bx-columns bx-18px me-2"></span>Columns',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
columnText: function (dt, idx, title) {
|
||||
return idx + 1 + ". " + title;
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "colvisRestore",
|
||||
text: '<span class="tf-icons bx bx-reset bx-18px me-2"></span>Reset<span class="d-none d-md-inline"> columns</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
},
|
||||
{
|
||||
extend: "collection",
|
||||
text: '<span class="tf-icons bx bx-export bx-18px me-2"></span>Export',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
buttons: [
|
||||
{
|
||||
extend: "copy",
|
||||
text: '<span class="tf-icons bx bx-copy bx-18px me-2"></span>Copy current page',
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
page: "current",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "csv",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>CSV',
|
||||
bom: true,
|
||||
filename: "bw_custom_configs",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "excel",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>Excel',
|
||||
filename: "bw_custom_configs",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
extend: "collection",
|
||||
text: '<span class="tf-icons bx bx-play bx-18px me-2"></span>Actions',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
buttons: [
|
||||
{
|
||||
extend: "delete_configs",
|
||||
className: "text-danger",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
extend: "create_config",
|
||||
},
|
||||
];
|
||||
|
||||
$(document).on("hidden.bs.toast", ".toast", function (event) {
|
||||
if (event.target.id.startsWith("feedback-toast")) {
|
||||
setTimeout(() => {
|
||||
$(this).remove();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
$("#modal-delete-configs").on("hidden.bs.modal", function () {
|
||||
$("#selected-configs-delete").empty();
|
||||
$("#selected-configs-input-delete").val("");
|
||||
});
|
||||
|
||||
const getSelectedConfigs = () => {
|
||||
const configs = [];
|
||||
$("tr.selected").each(function () {
|
||||
const $this = $(this);
|
||||
const name = $this.find("td:eq(1)").find("a").text().trim();
|
||||
const type = $this.find("td:eq(2)").text().trim();
|
||||
let service = $this.find("td:eq(4)");
|
||||
if (service.find("a").length > 0) {
|
||||
service = service.find("a").text().trim();
|
||||
} else {
|
||||
service = service.text().trim();
|
||||
}
|
||||
configs.push({ name: name, type: type, service: service });
|
||||
});
|
||||
return configs;
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.create_config = {
|
||||
text: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Create<span class="d-none d-md-inline"> new custom config</span>',
|
||||
className: "btn btn-sm btn-outline-bw-green",
|
||||
action: function (e, dt, node, config) {
|
||||
window.location.href = `${window.location.href}/new`;
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete_configs = {
|
||||
text: '<span class="tf-icons bx bx-trash bx-18px me-2"></span>Delete',
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
$(".dt-button-background").click();
|
||||
|
||||
const configs = getSelectedConfigs();
|
||||
if (configs.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
setupDeletionModal(configs);
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
};
|
||||
|
||||
const configs_table = new DataTable("#configs", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("text-center align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[1, "desc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:first-child",
|
||||
headerCheckbox: false,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ custom configs",
|
||||
infoEmpty: "No custom configs available",
|
||||
infoFiltered: "(filtered from _MAX_ total custom configs)",
|
||||
lengthMenu: "Display _MENU_ custom configs",
|
||||
zeroRecords: "No matching custom configs found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d custom configs",
|
||||
0: "No custom configs selected",
|
||||
1: "Selected 1 custom config",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#configs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
});
|
||||
|
||||
configs_table.on("mouseenter", "td", function () {
|
||||
const rowIdx = configs_table.cell(this).index().row;
|
||||
|
||||
configs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
configs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (configs_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
configs_table.on("mouseleave", "td", function () {
|
||||
configs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
// Event listener for the select-all checkbox
|
||||
$("#select-all-rows").on("change", function () {
|
||||
const isChecked = $(this).prop("checked");
|
||||
|
||||
if (isChecked) {
|
||||
// Select all rows on the current page
|
||||
configs_table.rows({ page: "current" }).select();
|
||||
} else {
|
||||
// Deselect all rows on the current page
|
||||
configs_table.rows({ page: "current" }).deselect();
|
||||
}
|
||||
});
|
||||
|
||||
$(".delete-config").on("click", function () {
|
||||
const config = {
|
||||
name: $(this).data("config-name"),
|
||||
type: $(this).data("config-type"),
|
||||
service: $(this).data("config-service"),
|
||||
};
|
||||
console.log(config);
|
||||
setupDeletionModal([config]);
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,92 @@ $(document).ready(function () {
|
|||
var actionLock = false;
|
||||
const instanceNumber = parseInt($("#instances_number").val());
|
||||
|
||||
const pingInstances = (instances) => {
|
||||
setTimeout(() => {
|
||||
if (actionLock) {
|
||||
$(".dt-button-background").click();
|
||||
$("#loadingModal").modal("show");
|
||||
setTimeout(() => {
|
||||
$("#loadingModal").modal("hide");
|
||||
}, 5300);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Create a FormData object
|
||||
const formData = new FormData();
|
||||
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
|
||||
$.ajax({
|
||||
url: `${window.location.pathname}/ping`,
|
||||
type: "POST",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (data) {
|
||||
data.failed.forEach((instance) => {
|
||||
var feedbackToastFailed = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToastFailed.attr("id", `feedback-toast-${toastNum++}`); // Corrected to set the ID for the failed toast
|
||||
feedbackToastFailed.removeClass("bg-primary text-white");
|
||||
feedbackToastFailed.addClass("bg-danger text-white");
|
||||
feedbackToastFailed.find("span").text("Ping failed");
|
||||
feedbackToastFailed.find("div.toast-body").text(instance.message);
|
||||
feedbackToastFailed.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
feedbackToastFailed.toast("show");
|
||||
});
|
||||
|
||||
if (data.succeed.length > 0) {
|
||||
var feedbackToastSucceed = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToastSucceed.attr("id", `feedback-toast-${toastNum++}`);
|
||||
feedbackToastSucceed.addClass("bg-primary text-white");
|
||||
feedbackToastSucceed.find("span").text("Ping successful");
|
||||
feedbackToastSucceed
|
||||
.find("div.toast-body")
|
||||
.text(`Instances: ${data.succeed.join(", ")}`);
|
||||
feedbackToastSucceed.appendTo("#feedback-toast-container");
|
||||
feedbackToastSucceed.toast("show");
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error("AJAX request failed:", status, error);
|
||||
alert("An error occurred while pinging the instances.");
|
||||
},
|
||||
complete: function () {
|
||||
actionLock = false;
|
||||
$("#loadingModal").modal("hide");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const execForm = (instances, action) => {
|
||||
// Create a form element using jQuery and set its attributes
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: `${window.location.pathname}/${action}`,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
// Add CSRF token and instances as hidden inputs
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "instances",
|
||||
value: instances.join(","),
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
form.appendTo("body").submit();
|
||||
};
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
|
|
@ -142,61 +228,7 @@ $(document).ready(function () {
|
|||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (actionLock) {
|
||||
$(".dt-button-background").click();
|
||||
$("#loadingModal").modal("show");
|
||||
setTimeout(() => {
|
||||
$("#loadingModal").modal("hide");
|
||||
}, 5300);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Create a FormData object
|
||||
const formData = new FormData();
|
||||
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
|
||||
$.ajax({
|
||||
url: `${window.location.pathname}/ping`,
|
||||
type: "POST",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (data) {
|
||||
data.failed.forEach((instance) => {
|
||||
var feedbackToastFailed = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToastFailed.attr("id", `feedback-toast-${toastNum++}`); // Corrected to set the ID for the failed toast
|
||||
feedbackToastFailed.removeClass("bg-primary text-white");
|
||||
feedbackToastFailed.addClass("bg-danger text-white");
|
||||
feedbackToastFailed.find("span").text("Ping failed");
|
||||
feedbackToastFailed.find("div.toast-body").text(instance.message);
|
||||
feedbackToastFailed.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
feedbackToastFailed.toast("show");
|
||||
});
|
||||
|
||||
if (data.succeed.length > 0) {
|
||||
var feedbackToastSucceed = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToastSucceed.attr("id", `feedback-toast-${toastNum++}`);
|
||||
feedbackToastSucceed.addClass("bg-primary text-white");
|
||||
feedbackToastSucceed.find("span").text("Ping successful");
|
||||
feedbackToastSucceed
|
||||
.find("div.toast-body")
|
||||
.text(`Instances: ${data.succeed.join(", ")}`);
|
||||
feedbackToastSucceed.appendTo("#feedback-toast-container");
|
||||
feedbackToastSucceed.toast("show");
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error("AJAX request failed:", status, error);
|
||||
alert("An error occurred while pinging the instances.");
|
||||
},
|
||||
complete: function () {
|
||||
actionLock = false;
|
||||
$("#loadingModal").modal("hide");
|
||||
},
|
||||
});
|
||||
pingInstances(instances);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -223,31 +255,7 @@ $(document).ready(function () {
|
|||
return;
|
||||
}
|
||||
|
||||
// Create a form element using jQuery and set its attributes
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: `${window.location.pathname}/${action}`,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
// Add CSRF token and instances as hidden inputs
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "instances",
|
||||
value: instances.join(","),
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
form.appendTo("body").submit();
|
||||
execForm(instances, action);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -268,22 +276,45 @@ $(document).ready(function () {
|
|||
|
||||
$("#selected-instances-input").val(instances.join(","));
|
||||
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Hostname</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Health</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-instances").append(list);
|
||||
|
||||
const delete_modal = $("#modal-delete-instances");
|
||||
instances.forEach((instance) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
$(`<li class="list-group-item align-items-center text-center" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${instance}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
list.append(listItem);
|
||||
|
||||
// Clone the status element and append it to the list item
|
||||
const statusClone = $("#status-" + instance).clone();
|
||||
listItem.append(statusClone);
|
||||
const statusListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`,
|
||||
);
|
||||
statusListItem.append(statusClone.removeClass("highlight"));
|
||||
list.append(statusListItem);
|
||||
|
||||
// Append the list item to the list
|
||||
$("#selected-instances").append(listItem);
|
||||
$("#selected-instances").append(list);
|
||||
});
|
||||
|
||||
const modal = new bootstrap.Modal(delete_modal);
|
||||
|
|
@ -300,6 +331,14 @@ $(document).ready(function () {
|
|||
render: DataTable.render.select(),
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: [2, 3],
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
|
|
@ -371,4 +410,20 @@ $(document).ready(function () {
|
|||
instances_table.rows({ page: "current" }).deselect();
|
||||
}
|
||||
});
|
||||
|
||||
$(".ping-instance").on("click", function () {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
|
||||
const instance = $(this).data("instance");
|
||||
pingInstances([instance]);
|
||||
});
|
||||
|
||||
$(".reload-instance, .stop-instance").on("click", function () {
|
||||
const instance = $(this).data("instance");
|
||||
const action = $(this).hasClass("reload-instance") ? "reload" : "stop";
|
||||
execForm([instance], action);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,86 @@ $(document).ready(function () {
|
|||
var actionLock = false;
|
||||
const serviceNumber = parseInt($("#services_number").val());
|
||||
|
||||
const setupModal = (services, modal) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Service name</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Type</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
);
|
||||
modal.append(list);
|
||||
|
||||
services.forEach((service) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item align-items-center text-center" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${service}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
list.append(listItem);
|
||||
|
||||
// Clone the type element and append it to the list item
|
||||
const typeClone = $("#type-" + service.replace(/\./g, "-")).clone();
|
||||
const typeListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`,
|
||||
);
|
||||
typeListItem.append(typeClone.removeClass("highlight"));
|
||||
list.append(typeListItem);
|
||||
|
||||
// Append the list item to the list
|
||||
modal.append(list);
|
||||
});
|
||||
};
|
||||
|
||||
const setupConversionModal = (services, convertionType = "draft") => {
|
||||
$("#selected-services-input-convert").val(services.join(","));
|
||||
|
||||
setupModal(services, $("#selected-services-convert"));
|
||||
|
||||
const convert_modal = $("#modal-convert-services");
|
||||
convert_modal
|
||||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to convert the selected service${"s".repeat(
|
||||
services.length > 1,
|
||||
)} to ${convertionType}?`,
|
||||
);
|
||||
convert_modal
|
||||
.find("button[type=submit]")
|
||||
.text(`Convert to ${convertionType}`);
|
||||
$("#convertion-type").val(convertionType);
|
||||
const modal = new bootstrap.Modal(convert_modal);
|
||||
modal.show();
|
||||
};
|
||||
|
||||
const setupDeletionModal = (services) => {
|
||||
$("#selected-services-input-delete").val(services.join(","));
|
||||
|
||||
setupModal(services, $("#selected-services-delete"));
|
||||
|
||||
const convert_modal = $("#modal-delete-services");
|
||||
const modal = new bootstrap.Modal(convert_modal);
|
||||
convert_modal
|
||||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to delete the selected service${"s".repeat(
|
||||
services.length > 1,
|
||||
)}?`,
|
||||
);
|
||||
modal.show();
|
||||
};
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
|
|
@ -18,7 +98,7 @@ $(document).ready(function () {
|
|||
layout.topStart.buttons = [
|
||||
{
|
||||
extend: "colvis",
|
||||
columns: "th:not(:first-child):not(:nth-child(2))",
|
||||
columns: "th:not(:first-child):not(:nth-child(2)):not(:last-child)",
|
||||
text: '<span class="tf-icons bx bx-columns bx-18px me-2"></span>Columns',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
columnText: function (dt, idx, title) {
|
||||
|
|
@ -72,9 +152,6 @@ $(document).ready(function () {
|
|||
text: '<span class="tf-icons bx bx-play bx-18px me-2"></span>Actions',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
buttons: [
|
||||
{
|
||||
extend: "clone_service",
|
||||
},
|
||||
{
|
||||
extend: "convert_services",
|
||||
text: '<span class="tf-icons bx bx-globe bx-18px me-2"></span>Convert to<span class="d-none d-md-inline"> online</span>',
|
||||
|
|
@ -128,36 +205,6 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.clone_service = {
|
||||
text: '<span class="tf-icons bx bx-copy-alt bx-18px me-2"></span>Clone<span class="d-none d-md-inline"> service</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
const services = getSelectedservices();
|
||||
if (services.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
} else if (services.length > 1) {
|
||||
const feedbackToast = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToast.attr("id", `feedback-toast-${toastNum++}`); // Corrected to set the ID for the failed toast
|
||||
feedbackToast.removeClass("bg-primary text-white");
|
||||
feedbackToast.addClass("bg-primary text-white");
|
||||
feedbackToast.find("span").text("Clone failed");
|
||||
feedbackToast
|
||||
.find("div.toast-body")
|
||||
.text("Only one service can be cloned at a time.");
|
||||
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
feedbackToast.toast("show");
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `${window.location.href}/new?clone=${services[0]}`;
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.convert_services = {
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
|
|
@ -192,44 +239,7 @@ $(document).ready(function () {
|
|||
return;
|
||||
}
|
||||
|
||||
$("#selected-services-input-convert").val(services.join(","));
|
||||
|
||||
services.forEach((service) => {
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${service}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
|
||||
// Clone the type element and append it to the list item
|
||||
const typeClone = $("#type-" + service.replace(/\./g, "-")).clone();
|
||||
listItem.append(typeClone);
|
||||
|
||||
// Append the list item to the list
|
||||
$("#selected-services-convert").append(listItem);
|
||||
});
|
||||
|
||||
const convert_modal = $("#modal-convert-services");
|
||||
convert_modal
|
||||
.find(".modal-title")
|
||||
.text(
|
||||
`Convert services to ${
|
||||
convertionType.charAt(0).toUpperCase() + convertionType.slice(1)
|
||||
}`,
|
||||
);
|
||||
convert_modal
|
||||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to convert the selected services to ${convertionType}?`,
|
||||
);
|
||||
convert_modal
|
||||
.find("button[type=submit]")
|
||||
.text(`Convert to ${convertionType}`);
|
||||
$("#convertion-type").val(convertionType);
|
||||
const modal = new bootstrap.Modal(convert_modal);
|
||||
modal.show();
|
||||
setupConversionModal(filteredServices, convertionType);
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
|
|
@ -250,28 +260,7 @@ $(document).ready(function () {
|
|||
return;
|
||||
}
|
||||
|
||||
$("#selected-services-input-delete").val(services.join(","));
|
||||
|
||||
const delete_modal = $("#modal-delete-services");
|
||||
services.forEach((service) => {
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${service}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
|
||||
// Clone the type element and append it to the list item
|
||||
const typeClone = $("#type-" + service.replace(/\./g, "-")).clone();
|
||||
listItem.append(typeClone);
|
||||
|
||||
// Append the list item to the list
|
||||
$("#selected-services-delete").append(listItem);
|
||||
});
|
||||
|
||||
const modal = new bootstrap.Modal(delete_modal);
|
||||
modal.show();
|
||||
setupDeletionModal(services);
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
|
|
@ -284,6 +273,10 @@ $(document).ready(function () {
|
|||
render: DataTable.render.select(),
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
|
|
@ -355,4 +348,15 @@ $(document).ready(function () {
|
|||
services_table.rows({ page: "current" }).deselect();
|
||||
}
|
||||
});
|
||||
|
||||
$(".convert-service").on("click", function () {
|
||||
const service = $(this).data("service-id");
|
||||
const convertionType = $(this).data("value");
|
||||
setupConversionModal([service], convertionType);
|
||||
});
|
||||
|
||||
$(".delete-service").on("click", function () {
|
||||
const service = $(this).data("service-id");
|
||||
setupDeletionModal([service]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ $(document).ready(() => {
|
|||
var toastNum = 0;
|
||||
let currentPlugin = "general";
|
||||
let currentStep = 1;
|
||||
let usedTemplate = $("#used-template").val().trim();
|
||||
|
||||
const $templateInput = $("#used-template");
|
||||
let usedTemplate = "advanced";
|
||||
if ($templateInput.length) {
|
||||
usedTemplate = $templateInput.val().trim();
|
||||
}
|
||||
|
||||
let currentTemplate = $("#selected-template").val();
|
||||
let currentMode = $("#selected-mode").val();
|
||||
let currentType = $("#selected-type").val();
|
||||
|
|
@ -367,7 +373,7 @@ $(document).ready(() => {
|
|||
|
||||
$("#select-plugin").on("click", () => $pluginSearch.focus());
|
||||
|
||||
$("#plugin-search").on(
|
||||
$pluginSearch.on(
|
||||
"input",
|
||||
debounce((e) => {
|
||||
const inputValue = e.target.value.toLowerCase();
|
||||
|
|
@ -575,7 +581,6 @@ $(document).ready(() => {
|
|||
.find(".multiple-collapse")
|
||||
.attr("id")
|
||||
.replace(`${multipleId}-`, ""),
|
||||
10,
|
||||
);
|
||||
})
|
||||
.get()
|
||||
|
|
@ -668,7 +673,8 @@ $(document).ready(() => {
|
|||
|
||||
// Remove "add-multiple" button and append the "REMOVE" button
|
||||
multipleClone.find(".add-multiple").remove();
|
||||
multipleClone.find(".show-multiple").before(`
|
||||
const multipleShow = multipleClone.find(".show-multiple");
|
||||
multipleShow.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
|
||||
|
|
@ -684,7 +690,6 @@ $(document).ready(() => {
|
|||
.find(".multiple-collapse")
|
||||
.attr("id")
|
||||
.replace(`${multipleId}-`, ""),
|
||||
10,
|
||||
);
|
||||
if (containerSuffix > suffix) {
|
||||
$(this).before(multipleClone); // Insert before the first container with a higher suffix
|
||||
|
|
@ -693,6 +698,9 @@ $(document).ready(() => {
|
|||
}
|
||||
});
|
||||
|
||||
multipleShow.html(`<i class="bx bx-hide bx-sm"></i> SHOW`);
|
||||
multipleClone.find(".multiple-collapse").collapse("hide");
|
||||
|
||||
if (!inserted) {
|
||||
// If no higher suffix was found, append to the end
|
||||
$(`#${multipleId}`).append(multipleClone);
|
||||
|
|
@ -707,9 +715,10 @@ $(document).ready(() => {
|
|||
.attr("data-bs-target", `#${cloneId}`)
|
||||
.attr("aria-controls", cloneId);
|
||||
|
||||
if (showMultiple.text().trim() === "SHOW") showMultiple.trigger("click");
|
||||
|
||||
highlightSettings(multipleClone);
|
||||
setTimeout(() => {
|
||||
showMultiple.trigger("click");
|
||||
highlightSettings(multipleClone);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
$(document).on("click", ".remove-multiple", function () {
|
||||
|
|
@ -758,17 +767,11 @@ $(document).ready(() => {
|
|||
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");
|
||||
} else if (currentMode === "raw") {
|
||||
const wasDraft = draftInput.data("original") === "yes";
|
||||
let isDraft = draftInput.val() === "yes";
|
||||
if (currentMode === "raw")
|
||||
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
|
||||
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
|
||||
|
||||
if (form.children().length < minSettings && isDraft === wasDraft) {
|
||||
if (form.children().length < 2 && isDraft === wasDraft) {
|
||||
alert("No changes detected.");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='fonts/boxicons.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% if current_endpoint in ("instances", "services") %}
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache") %}
|
||||
<!-- Datatables -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/datatables/datatables.min.css') }}"
|
||||
|
|
@ -89,11 +89,11 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='libs/purify/purify.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% if current_endpoint in ("instances", "services") %}
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache") %}
|
||||
<script src="{{ url_for('static', filename='libs/datatables/datatables.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% if current_endpoint != "services" and "services" in request.path %}
|
||||
{% if current_endpoint not in ("services", "configs", "cache") and ("services" in request.path or "configs" in request.path or "cache" in request.path) %}
|
||||
<script src="{{ url_for('static', filename='libs/ace/src-min/ace.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
|
|
@ -131,6 +131,18 @@
|
|||
{% elif current_endpoint == "services" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/services.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "configs" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/configs.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint != "configs" and "configs" in request.path %}
|
||||
<script src="{{ url_for('static', filename='js/pages/config_edit.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "cache" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/cache.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint != "cache" and "cache" in request.path %}
|
||||
<script src="{{ url_for('static', filename='js/pages/cache_view.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
<script async defer src="{{ url_for('static', filename='js/buttons.js') }}"></script>
|
||||
</body>
|
||||
|
|
|
|||
90
src/ui/app/templates/cache.html
Normal file
90
src/ui/app/templates/cache.html
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="cache_number" value="{{ caches|length }}" />
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="cache" class="table w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The user defined Cache's file name">File name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Cache's job name">Job name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Cache's job's Plugin">Plugin</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Service associated with the Cache">Service</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The date and time when the Cache was last updated">Last Update</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Cache's checksum">Checksum</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The actions that can be performed on the Instance">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cache in caches %}
|
||||
{% set service_id = cache['service_id'] if cache['service_id'] else 'global' %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="View {{ cache['file_name'] }}"><i class="bx bx-show bx-xs"></i> {{ cache['file_name'] }}</a>
|
||||
</td>
|
||||
<td>{{ cache['job_name'] }}</td>
|
||||
<td>{{ cache['plugin_id'] }}</td>
|
||||
<td id="service-{{ cache['job_name'] }}-{{ service_id.replace('.', '_') }}-{{ cache['file_name'].replace('.', '_') }}">
|
||||
{% if cache["service_id"] %}
|
||||
<a href="{{ url_for("services") }}/{{ cache['service_id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit service {{ cache['service_id'] }}"><i class="bx bx-edit bx-xs"></i> {{ cache["service_id"] }}</a>
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-label-secondary">global</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ cache["last_update"] if cache["last_update"] == "Never" else cache["last_update"].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}
|
||||
</td>
|
||||
<td>{{ cache["checksum"] }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-sm me-1"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="View {{ cache['file_name'] }}">
|
||||
<i class="bx bx-show bx-xs"></i>
|
||||
</a>
|
||||
<a role="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') }}?download=true"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Download {{ cache['file_name'] }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<i class="bx bx-download bx-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
24
src/ui/app/templates/cache_view.html
Normal file
24
src/ui/app/templates/cache_view.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card position-relative p-4 min-vh-70">
|
||||
<div class="position-absolute top-0 end-0 m-2" style="z-index: 1000">
|
||||
<div class="card p-1 me-2">
|
||||
<a role="button"
|
||||
class="btn btn-sm btn-outline-secondary "
|
||||
href="{{ url_for("cache") }}/{{ '/'.join(request.path.split('/')[2:]) }}?download=true"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Download file"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<i class="bx bx-download bx-xs"></i>
|
||||
<span class="d-none d-md-inline"> Download</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cache-value"
|
||||
class="ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ cache_file }}</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
142
src/ui/app/templates/config_edit.html
Normal file
142
src/ui/app/templates/config_edit.html
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<input type="hidden"
|
||||
id="selected-service"
|
||||
name="selected_service"
|
||||
value="{{ config_service if config_service and config_service != "global" else "no service" }}">
|
||||
<input type="hidden"
|
||||
id="selected-type"
|
||||
name="selected_type"
|
||||
value="{{ type if type else "HTTP" }}">
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}">
|
||||
<div class="card p-1 mb-4 sticky-card">
|
||||
<div class="d-flex flex-wrap justify-content-around align-items-center">
|
||||
<div class="d-flex">
|
||||
{% if not config_template and config_method and config_method != "ui" %}
|
||||
<button type="button" class="btn btn-sm btn-secondary ms-2 disabled">
|
||||
<i class="bx bx-xs bx-cube"></i>
|
||||
Service: {{ config_service or "no service" }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary ms-2 disabled">
|
||||
<i class="bx bx-xs bx-window"></i>
|
||||
Config Type: {{ type }}
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="dropdown btn-group me-2">
|
||||
<button id="select-service"
|
||||
type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="bx bx-cube"></i>
|
||||
<span class="d-none d-md-inline"> Service</span>
|
||||
</button>
|
||||
<ul id="services-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="service-search"
|
||||
type="text"
|
||||
class="form-control border-0 border-primary border-bottom shadow-none"
|
||||
placeholder="Search..."
|
||||
aria-label="Search...">
|
||||
</div>
|
||||
<li class="nav-item">
|
||||
<button type="button"
|
||||
class="dropdown-item{% if not config_service or config_service == 'global' %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
{% if not config_service %}aria-selected="true"{% endif %}>no service</button>
|
||||
</li>
|
||||
{% for service in services %}
|
||||
<li class="nav-item">
|
||||
<button type="button"
|
||||
class="dropdown-item{% if config_service == service %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
{% if config_service == service %}aria-selected="true"{% endif %}>
|
||||
{{ service }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown btn-group">
|
||||
<button id="select-type"
|
||||
type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="bx bx-window"></i>
|
||||
<span class="d-none d-md-inline"> Config Type</span>
|
||||
</button>
|
||||
<ul id="types-dropdown-menu"
|
||||
class="dropdown-menu nav-pills max-vh-60 overflow-auto pt-0"
|
||||
role="tablist">
|
||||
{% for config_type, data in config_types.items() %}
|
||||
<li id="config-type-{{ config_type }}"
|
||||
class="nav-item"
|
||||
data-context="{{ data['context'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-original-title="{{ data['description'] }}">
|
||||
<button type="button"
|
||||
class="dropdown-item{% if config_type == type or not type and loop.index == 1 %} active{% endif %}"
|
||||
role="tab"
|
||||
data-bs-toggle="tab"
|
||||
{% if config_type == type or not type and loop.index == 1 %}aria-selected="true"{% endif %}>
|
||||
<i class="bx bx-{% if config_type.startswith('CRS') %}shield-alt{% elif config_type == 'MODSEC_CRS' %}shield-quarter{% elif config_type == 'MODSEC' %}shield-alt-2{% elif 'STREAM' in config_type %}network-chart{% else %}window-alt{% endif %}"></i> {{ config_type }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if config_template %}
|
||||
<button type="button" class="btn btn-sm btn-secondary ms-2 disabled">
|
||||
<i class="bx bx-xs bx-spreadsheet"></i>
|
||||
From template: {{ config_template }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row align-items-end">
|
||||
<div class="col form-floating input-group input-group-merge shadow-none">
|
||||
<input id="config-name"
|
||||
type="text"
|
||||
class="form-control form-control-sm border-0 border-primary border-bottom shadow-none"
|
||||
placeholder="Configuration Name"
|
||||
value="{{ name }}"
|
||||
pattern="^[a-zA-Z0-9_\-]{1,64}$"
|
||||
required
|
||||
{% if not config_template and config_method and config_method != "ui" %}disabled{% endif %}>
|
||||
<label for="config-name">Configuration Name</label>
|
||||
<span class="input-group-text border-0 border-primary border-bottom mt-2 pb-0 shadow-none"
|
||||
id="config-name-suffix">.conf</span>
|
||||
</div>
|
||||
</div>
|
||||
<div {% if not config_template and config_method and config_method != "ui" %}data-bs-toggle="tooltip" data-bs-placement="top" title="The custom config was created using the {{ config_method }} method, therefore it is locked"{% endif %}>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-bw-green save-config{% if not config_template and config_method and config_method != "ui" %} disabled{% endif %}">
|
||||
<i class="bx bx-save bx-sm"></i>
|
||||
<span class="d-none d-md-inline"> Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card position-relative p-4 min-vh-70"
|
||||
{% if not config_template and config_method and config_method != "ui" %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ config_method }}"{% endif %}>
|
||||
<div id="config-value"
|
||||
data-language="{% if type and type.startswith(('CRS', 'MODSEC')) %}ModSecurity{% else %}NGINX{% endif %}"
|
||||
data-method="{{ config_method }}"
|
||||
data-template="{{ config_template }}"
|
||||
class="ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ config_value }}</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
151
src/ui/app/templates/configs.html
Normal file
151
src/ui/app/templates/configs.html
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="configs_number" value="{{ configs|length }}" />
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="configs" class="table w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input id="select-all-rows"
|
||||
aria-label="Select all rows"
|
||||
class="dt-select-checkbox mb-1"
|
||||
type="checkbox">
|
||||
Select All
|
||||
</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The user defined Custom config's name">Name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Custom config's type">Type</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The creation method of the Custom config">Method</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Service associated with the Custom configuration">Service</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Indication if the custom config is from a template or not">
|
||||
Template
|
||||
</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Custom config's checksum">Checksum</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The actions that can be performed on the Instance">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for config in configs %}
|
||||
{% set service_id = config['service_id'] if config['service_id'] else 'global' %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{ url_for("configs") }}/{{ service_id }}/{{ config['type'] }}/{{ config['name'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit custom config {{ config['name'] }}"><i class="bx bx-edit bx-xs"></i> {{ config["name"] }}</a>
|
||||
</td>
|
||||
<td id="type-{{ config['type'] }}-{{ service_id.replace('.', '_') }}-{{ config['name'] }}">
|
||||
<i class="bx bx-{% if config['type'].startswith('crs') %}shield-alt{% elif config['type'] == 'modsec_crs' %}shield-quarter{% elif config['type'] == 'modsec' %}shield-alt-2{% elif 'stream' in config['type'] %}network-chart{% else %}window-alt{% endif %}"></i>
|
||||
{{ config["type"]|upper }}
|
||||
</td>
|
||||
<td>{{ config["method"] }}</td>
|
||||
<td id="service-{{ config['type'] }}-{{ service_id.replace('.', '_') }}-{{ config['name'] }}">
|
||||
{% if config["service_id"] %}
|
||||
<a href="{{ url_for("services") }}/{{ config['service_id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit service {{ config['service_id'] }}"><i class="bx bx-edit bx-xs"></i> {{ config["service_id"] }}</a>
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-label-secondary">global</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if config["template"] %}
|
||||
<span class="badge rounded-pill bg-label-primary">{{ config["template"] }}</span>
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-label-secondary">no template</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ config["checksum"] }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-sm me-1"
|
||||
href="{{ url_for("configs") }}/{{ service_id }}/{{ config['type'] }}/{{ config['name'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit custom config {{ config['name'] }}">
|
||||
<i class="bx bx-edit bx-xs"></i>
|
||||
</a>
|
||||
<a role="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1"
|
||||
href="{{ url_for("configs") }}/new?clone={{ service_id }}/{{ config['type'] }}/{{ config['name'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Clone custom config {{ config['name'] }}">
|
||||
<i class="bx bx-copy-alt bx-xs"></i>
|
||||
</a>
|
||||
<div data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="{% if config['method'] != 'ui' %}Disabled by {% if config['template'] %}template: {{ config['template'] }}{% else %}{{ config['method'] }}{% endif %}{% else %}Delete custom config {{ config['name'] }}{% endif %}">
|
||||
<button type="button"
|
||||
data-config-name="{{ config['name'] }}"
|
||||
data-config-type="{{ config['type'] }}"
|
||||
data-config-service="{{ config['service_id'] or 'global' }}"
|
||||
class="btn btn-outline-danger btn-sm me-1 delete-config{% if config['method'] != 'ui' %} disabled{% endif %}">
|
||||
<i class="bx bx-trash bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal modal-lg fade"
|
||||
id="modal-delete-configs"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
role="dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirm deletion</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for("configs") }}/delete" method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden"
|
||||
id="selected-configs-input-delete"
|
||||
name="configs"
|
||||
value="" />
|
||||
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected configs?</div>
|
||||
<div id="selected-configs-delete" class="mb-3"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Delete</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
@ -54,6 +54,9 @@
|
|||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The date and time when the Instance was last seen">Last Seen</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The actions that can be performed on the Instance">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -86,6 +89,45 @@
|
|||
</td>
|
||||
<td>{{ instance.creation_date.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>{{ instance.last_seen.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<button type="button"
|
||||
class="btn btn-primary btn-sm me-1 ping-instance"
|
||||
data-instance="{{ instance['hostname'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Ping Instance {{ instance['hostname'] }}">
|
||||
<i class="bx bx-bell bx-xs"></i>
|
||||
</button>
|
||||
<div data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="{% if instance.status != 'up' %}Instance {{ instance['hostname'] }} is not up{% else %}Reload instance {{ instance['hostname'] }}{% endif %}">
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1 reload-instance{% if instance.status != 'up' %} disabled{% endif %}"
|
||||
data-instance="{{ instance['hostname'] }}">
|
||||
<i class="bx bx-refresh bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="{% if instance.status != 'up' %}Instance {{ instance['hostname'] }} is not up{% else %}Stop instance {{ instance['hostname'] }}{% endif %}">
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1 stop-instance{% if instance.status != 'up' %} disabled{% endif %}"
|
||||
data-instance="{{ instance['hostname'] }}">
|
||||
<i class="bx bx-stop bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="{% if instance['method'] != 'ui' %}Disabled by {{ instance['method'] }}{% else %}Delete instance {{ instance['hostname'] }}{% endif %}">
|
||||
<button type="button"
|
||||
data-instance="{{ instance['hostname'] }}"
|
||||
class="btn btn-outline-danger btn-sm me-1 delete-instance{% if instance['method'] != 'ui' %} disabled{% endif %}">
|
||||
<i class="bx bx-trash bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -180,8 +222,7 @@
|
|||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" id="selected-instances-input" name="instances" value="" />
|
||||
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected instances?</div>
|
||||
<ul id="selected-instances" class="list-group mb-3">
|
||||
</ul>
|
||||
<div id="selected-instances" class="mb-3"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Delete instances</button>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
} %}
|
||||
<ul class="menu-inner py-1">
|
||||
{% for endpoint, item in menu_items.items() %}
|
||||
<li class="menu-item {% if endpoint in request.path %}active{% endif %} {% if item.get('sub') and item.get('open', True) %}open{% endif %}">
|
||||
<li class="menu-item {% if endpoint in request.path.split('/')[1] %}active{% endif %} {% if item.get('sub') and item.get('open', True) %}open{% endif %}">
|
||||
<a href="{{ item['url'] }}"
|
||||
class="menu-link {% if item.get('sub') %}menu-toggle{% endif %}">
|
||||
<i class="menu-icon tf-icons bx {{ item['icon'] }}"></i>
|
||||
|
|
|
|||
|
|
@ -224,8 +224,7 @@
|
|||
{% for multiple, multiples in plugin_multiples.items() %}
|
||||
{% set multiple_settings = settings|length > 1 %}
|
||||
<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 -->
|
||||
class="col-12{% if multiple_plugin_multiples %} col-md-6{% endif %}{% if plugin_multiples|length > 2 and not multiple_settings %} col-lg-4{% endif %}">
|
||||
{% for setting_suffix, settings in multiples.items() %}
|
||||
<div class="row multiple-container pt-2 pb-2">
|
||||
{% set setting_id_suffix = "-" + setting_suffix %}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@
|
|||
data-bs-original-title="The name of the service">Service name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The creation method of the Service">Method</th>
|
||||
data-bs-original-title="The type of the Service">Type</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The type of the Service">Type</th>
|
||||
data-bs-original-title="The creation method of the Service">Method</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The date and time when the Service was created">Created</th>
|
||||
|
|
@ -47,7 +47,6 @@
|
|||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit service {{ service['id'] }}"><i class="bx bx-edit bx-xs"></i> {{ service["id"] }}</a>
|
||||
</td>
|
||||
<td id="method-{{ service['id'].replace('.', '-') }}">{{ service["method"] }}</td>
|
||||
<td>
|
||||
{% if service['is_draft'] %}
|
||||
<span id="type-{{ service['id'].replace('.', '-') }}"
|
||||
|
|
@ -59,28 +58,41 @@
|
|||
class="badge rounded-pill bg-label-primary">Online</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td id="method-{{ service['id'].replace('.', '-') }}">{{ service["method"] }}</td>
|
||||
<td>{{ service['creation_date'].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>{{ service['last_update'].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-sm me-1"
|
||||
href="{{ url_for("services") }}/{{ service['id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit service {{ service['id'] }}">
|
||||
class="btn btn-outline-primary btn-sm me-1"
|
||||
href="https://{{ service['id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Access service {{ service['id'] }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<i class="bx bx-link-external bx-xs"></i>
|
||||
</a>
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-sm me-1"
|
||||
href="{{ url_for("services") }}/{{ service['id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Edit service {{ service['id'] }}">
|
||||
<i class="bx bx-edit bx-xs"></i>
|
||||
</a>
|
||||
<a role="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1"
|
||||
href="{{ url_for("services") }}/new?clone={{ service['id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Clone service {{ service['id'] }}">
|
||||
class="btn btn-outline-secondary btn-sm me-1"
|
||||
href="{{ url_for("services") }}/new?clone={{ service['id'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Clone service {{ service['id'] }}">
|
||||
<i class="bx bx-copy-alt bx-xs"></i>
|
||||
</a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1"
|
||||
class="btn btn-outline-secondary btn-sm me-1 convert-service"
|
||||
data-service-id="{{ service['id'] }}"
|
||||
data-value="{{ 'online' if service['is_draft'] else 'draft' }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Convert service {{ service['id'] }} to {{ 'online' if service['is_draft'] else 'draft' }}">
|
||||
|
|
@ -90,7 +102,8 @@
|
|||
data-bs-placement="bottom"
|
||||
data-bs-original-title="{% if service['method'] != 'ui' %}Disabled by {{ service['method'] }}{% else %}Delete service {{ service['id'] }}{% endif %}">
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm me-1{% if service['method'] != 'ui' %} disabled{% endif %}">
|
||||
data-service-id="{{ service['id'] }}"
|
||||
class="btn btn-outline-danger btn-sm me-1 delete-service{% if service['method'] != 'ui' %} disabled{% endif %}">
|
||||
<i class="bx bx-trash bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -127,7 +140,7 @@
|
|||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Convert services to Draft</h5>
|
||||
<h5 class="modal-title">Confirm Conversion</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
|
|
@ -142,11 +155,13 @@
|
|||
name="services"
|
||||
value="" />
|
||||
<div class="alert alert-danger text-center" role="alert">Are you sure you want to convert the selected services?</div>
|
||||
<ul id="selected-services-convert" class="list-group mb-3">
|
||||
</ul>
|
||||
<div id="selected-services-convert" class="mb-3"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-secondary me-2">Convert services</button>
|
||||
<button type="submit" class="btn btn-outline-success me-2">Convert services</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -161,7 +176,7 @@
|
|||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Delete services</h5>
|
||||
<h5 class="modal-title">Confirm deletion</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
|
|
@ -175,14 +190,13 @@
|
|||
name="services"
|
||||
value="" />
|
||||
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected services?</div>
|
||||
<ul id="selected-services-delete" class="list-group mb-3">
|
||||
</ul>
|
||||
<div id="selected-services-delete" class="mb-3"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Delete services</button>
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Delete</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -229,8 +229,8 @@ def handle_csrf_error(_):
|
|||
logout_user()
|
||||
flash("Wrong CSRF token !", "error")
|
||||
if not current_user:
|
||||
return render_template("setup.html"), 403
|
||||
return render_template("login.html"), 403
|
||||
return redirect(url_for("setup.setup_page")), 403
|
||||
return redirect(url_for("login.login_page")), 403
|
||||
|
||||
|
||||
def update_latest_stable_release():
|
||||
|
|
|
|||
Loading…
Reference in a new issue