mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add services page to web UI and finish raw mode for service settings v1
This commit is contained in:
parent
37cdd322af
commit
27152f0977
19 changed files with 1108 additions and 131 deletions
|
|
@ -1349,13 +1349,14 @@ class Database:
|
|||
|
||||
for draft in drafts:
|
||||
if draft not in db_drafts:
|
||||
current_time = datetime.now().astimezone()
|
||||
if draft not in db_ids:
|
||||
self.logger.debug(f"Adding draft {draft}")
|
||||
to_put.append(Services(id=draft, method=method, is_draft=True))
|
||||
to_put.append(Services(id=draft, method=method, is_draft=True, creation_date=current_time, last_update=current_time))
|
||||
db_ids[draft] = {"method": method, "is_draft": True}
|
||||
elif method == db_ids[draft]["method"]:
|
||||
self.logger.debug(f"Updating draft {draft}")
|
||||
session.query(Services).filter(Services.id == draft).update({Services.is_draft: True})
|
||||
session.query(Services).filter(Services.id == draft).update({Services.is_draft: True, Services.last_update: current_time})
|
||||
changed_services = True
|
||||
|
||||
template = config.get("USE_TEMPLATE", "")
|
||||
|
|
@ -1385,7 +1386,12 @@ class Database:
|
|||
|
||||
if server_name not in db_ids:
|
||||
self.logger.debug(f"Adding service {server_name}")
|
||||
to_put.append(Services(id=server_name, method=method, is_draft=server_name in drafts))
|
||||
current_time = datetime.now().astimezone()
|
||||
to_put.append(
|
||||
Services(
|
||||
id=server_name, method=method, is_draft=server_name in drafts, creation_date=current_time, last_update=current_time
|
||||
)
|
||||
)
|
||||
db_ids[server_name] = {"method": method, "is_draft": server_name in drafts}
|
||||
if server_name not in drafts:
|
||||
changed_services = True
|
||||
|
|
@ -1425,6 +1431,7 @@ class Database:
|
|||
self.logger.debug(f"Adding setting {key} for service {server_name}")
|
||||
changed_plugins.add(setting.plugin_id)
|
||||
to_put.append(Services_settings(service_id=server_name, setting_id=key, value=value, suffix=suffix, method=method))
|
||||
session.query(Services).filter(Services.id == server_name).update({Services.last_update: datetime.now().astimezone()})
|
||||
elif (
|
||||
method == service_setting.method or (service_setting.method not in ("scheduler", "autoconf") and method == "autoconf")
|
||||
) and service_setting.value != value:
|
||||
|
|
@ -1446,6 +1453,7 @@ class Database:
|
|||
|
||||
self.logger.debug(f"Updating setting {key} for service {server_name}")
|
||||
query.update({Services_settings.value: value, Services_settings.method: method})
|
||||
session.query(Services).filter(Services.id == server_name).update({Services.last_update: datetime.now().astimezone()})
|
||||
elif setting and original_key not in global_values:
|
||||
global_values.append(original_key)
|
||||
global_value = (
|
||||
|
|
@ -1502,7 +1510,10 @@ class Database:
|
|||
|
||||
if not session.query(Services).with_entities(Services.id).filter_by(id=first_server).first():
|
||||
self.logger.debug(f"Adding service {first_server}")
|
||||
to_put.append(Services(id=first_server, method=method))
|
||||
current_time = datetime.now().astimezone()
|
||||
to_put.append(
|
||||
Services(id=first_server, method=method, is_draft=first_server in drafts, creation_date=current_time, last_update=current_time)
|
||||
)
|
||||
changed_services = True
|
||||
|
||||
for key, value in config.items():
|
||||
|
|
@ -2020,6 +2031,23 @@ class Database:
|
|||
|
||||
return services
|
||||
|
||||
def get_services(self, *, with_drafts: bool = False) -> List[Dict[str, Any]]:
|
||||
"""Get the services from the database"""
|
||||
with self._db_session() as session:
|
||||
return [
|
||||
{
|
||||
"id": service.id,
|
||||
"method": service.method,
|
||||
"is_draft": service.is_draft,
|
||||
"creation_date": service.creation_date,
|
||||
"last_update": service.last_update,
|
||||
}
|
||||
for service in session.query(Services).with_entities(
|
||||
Services.id, Services.method, Services.is_draft, Services.creation_date, Services.last_update
|
||||
)
|
||||
if with_drafts or not service.is_draft
|
||||
]
|
||||
|
||||
def add_job_run(self, job_name: str, success: bool, start_date: datetime, end_date: Optional[datetime] = None) -> str:
|
||||
"""Add a job run."""
|
||||
with self._db_session() as session:
|
||||
|
|
|
|||
|
|
@ -109,6 +109,8 @@ class Services(Base):
|
|||
id = Column(String(64), primary_key=True)
|
||||
method = Column(METHODS_ENUM, nullable=False)
|
||||
is_draft = Column(Boolean, default=False, nullable=False)
|
||||
creation_date = Column(DateTime(timezone=True), nullable=False)
|
||||
last_update = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
settings = relationship("Services_settings", back_populates="service", cascade="all")
|
||||
custom_configs = relationship("Custom_configs", back_populates="service", cascade="all")
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Config:
|
|||
self.__db = db
|
||||
self.__data = data
|
||||
|
||||
def __gen_conf(
|
||||
def gen_conf(
|
||||
self, global_conf: dict, services_conf: list[dict], *, check_changes: bool = True, changed_service: Optional[str] = None
|
||||
) -> Union[str, Set[str]]:
|
||||
"""Generates the nginx configuration file from the given configuration
|
||||
|
|
@ -108,7 +108,14 @@ class Config:
|
|||
return self.__db.get_services_settings(methods=methods, with_drafts=with_drafts)
|
||||
|
||||
def check_variables(
|
||||
self, variables: dict, config: dict, *, global_config: bool = False, ignored_multiples: Optional[Set[str]] = None, threaded: bool = False
|
||||
self,
|
||||
variables: dict,
|
||||
config: dict,
|
||||
*,
|
||||
global_config: bool = False,
|
||||
ignored_multiples: Optional[Set[str]] = None,
|
||||
new: bool = False,
|
||||
threaded: bool = False,
|
||||
) -> dict:
|
||||
"""Testify that the variables passed are valid
|
||||
|
||||
|
|
@ -124,6 +131,8 @@ class Config:
|
|||
"""
|
||||
self.__data.load_from_file()
|
||||
plugins_settings = self.get_plugins_settings()
|
||||
blacklisted_settings = get_blacklisted_settings(global_config)
|
||||
|
||||
for k, v in variables.copy().items():
|
||||
check = False
|
||||
|
||||
|
|
@ -140,7 +149,7 @@ class Config:
|
|||
variables.pop(k)
|
||||
continue
|
||||
|
||||
if setting in get_blacklisted_settings(global_config):
|
||||
if setting in blacklisted_settings:
|
||||
message = f"Variable {k} is not editable, ignoring it"
|
||||
if threaded:
|
||||
self.__data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
|
|
@ -151,7 +160,7 @@ class Config:
|
|||
elif setting not in config and plugins_settings[setting]["default"] == v:
|
||||
variables.pop(k)
|
||||
continue
|
||||
elif config[setting]["method"] not in ("default", "ui"):
|
||||
elif not new and setting != "IS_DRAFT" 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"})
|
||||
|
|
@ -219,7 +228,7 @@ class Config:
|
|||
return f"Service {service['SERVER_NAME'].split(' ')[0]} already exists.", 1
|
||||
|
||||
services.append(variables | {"IS_DRAFT": "yes" if is_draft else "no"})
|
||||
ret = self.__gen_conf(self.get_config(methods=False), services, check_changes=not is_draft)
|
||||
ret = self.gen_conf(self.get_config(methods=False), services, check_changes=not is_draft)
|
||||
if isinstance(ret, str):
|
||||
return ret, 1
|
||||
return f"Configuration for {variables['SERVER_NAME'].split(' ')[0]} has been generated.", 0
|
||||
|
|
@ -259,7 +268,7 @@ class Config:
|
|||
if k.startswith(old_server_name_splitted[0]):
|
||||
config.pop(k)
|
||||
|
||||
ret = self.__gen_conf(config, services, check_changes=check_changes, changed_service=server_name_splitted[0])
|
||||
ret = self.gen_conf(config, services, check_changes=check_changes, changed_service=server_name_splitted[0])
|
||||
if isinstance(ret, str):
|
||||
return ret, 1
|
||||
return f"Configuration for {old_server_name_splitted[0]} has been edited.", 0
|
||||
|
|
@ -277,7 +286,7 @@ class Config:
|
|||
str
|
||||
the confirmation message
|
||||
"""
|
||||
ret = self.__gen_conf(self.get_config(methods=False) | variables, self.get_services(methods=False), check_changes=check_changes)
|
||||
ret = self.gen_conf(self.get_config(methods=False) | variables, self.get_services(methods=False), check_changes=check_changes)
|
||||
if isinstance(ret, str):
|
||||
return ret, 1
|
||||
return "The global configuration has been edited.", 0
|
||||
|
|
@ -327,7 +336,7 @@ class Config:
|
|||
if k in service:
|
||||
service.pop(k)
|
||||
|
||||
ret = self.__gen_conf(new_env, new_services, check_changes=check_changes)
|
||||
ret = self.gen_conf(new_env, new_services, check_changes=check_changes)
|
||||
if isinstance(ret, str):
|
||||
return ret, 1
|
||||
return f"Configuration for {service_name} has been deleted.", 0
|
||||
|
|
|
|||
|
|
@ -2,38 +2,99 @@ from re import match
|
|||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Dict
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, Response, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, wait_applying
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, verify_data_in_form, wait_applying
|
||||
|
||||
services = Blueprint("services", __name__)
|
||||
|
||||
|
||||
@services.route("/services", methods=["GET", "POST"])
|
||||
@services.route("/services", methods=["GET"])
|
||||
@login_required
|
||||
def services_page():
|
||||
if request.method == "POST": # TODO: Handle creation and deletion of services
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
return render_template("services.html", services=DB.get_services(with_drafts=True))
|
||||
|
||||
# config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode = get_service_data("services")
|
||||
|
||||
# message = update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged)
|
||||
@services.route("/services/delete", methods=["POST"])
|
||||
@login_required
|
||||
def services_delete():
|
||||
verify_data_in_form(
|
||||
data={"services": None},
|
||||
err_message="Missing services parameter on /services/delete.",
|
||||
redirect_url="services",
|
||||
next=True,
|
||||
)
|
||||
services = request.form["services"].split(",")
|
||||
if not services:
|
||||
return handle_error("No services selected.", "services", True)
|
||||
DATA.load_from_file()
|
||||
|
||||
# return redirect(url_for("loading", next=url_for("services.services_page"), message=message))
|
||||
def delete_services(services):
|
||||
wait_applying()
|
||||
|
||||
return render_template("services.html") # TODO
|
||||
db_config = BW_CONFIG.get_config(methods=False)
|
||||
db_services = DB.get_services(with_drafts=True)
|
||||
all_drafts = True
|
||||
services_to_delete = set()
|
||||
non_ui_services = set()
|
||||
|
||||
for db_service in db_services:
|
||||
if db_service["id"] in services:
|
||||
if db_service["method"] != "ui":
|
||||
non_ui_services.add(db_service["id"])
|
||||
continue
|
||||
if not db_service["is_draft"]:
|
||||
all_drafts = False
|
||||
services_to_delete.add(db_service["id"])
|
||||
|
||||
for non_ui_service in non_ui_services:
|
||||
DATA["TO_FLASH"].append({"content": f"Service {non_ui_service} is not a UI service and will not be deleted.", "type": "error"})
|
||||
|
||||
if not services_to_delete:
|
||||
DATA["TO_FLASH"].append({"content": "All selected services could not be found or are not UI services.", "type": "error"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
db_config["SERVER_NAME"] = " ".join([service["id"] for service in db_services if service["id"] not in services_to_delete])
|
||||
new_env = db_config.copy()
|
||||
|
||||
for setting in db_config:
|
||||
for service in services_to_delete:
|
||||
if setting.startswith(f"{service}_"):
|
||||
del new_env[setting]
|
||||
|
||||
ret = BW_CONFIG.gen_conf(new_env, [], check_changes=not all_drafts)
|
||||
if isinstance(ret, str):
|
||||
DATA["TO_FLASH"].append({"content": ret, "type": "error"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
DATA["TO_FLASH"].append({"content": f"Deleted services {', '.join(services_to_delete)}", "type": "success"})
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
Thread(target=delete_services, args=(services,)).start()
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"loading",
|
||||
next=url_for("services.services_page"),
|
||||
message=f"Deleting services {', '.join(services)}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@services.route("/services/<string:service>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def services_service_page(service: str):
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, with_drafts=True, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
service_exists = service in services
|
||||
|
||||
if service != "new" and not service_exists:
|
||||
return Response("Service not found", status=404)
|
||||
|
||||
if request.method == "POST":
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
|
|
@ -44,54 +105,61 @@ def services_service_page(service: str):
|
|||
del variables["csrf_token"]
|
||||
|
||||
mode = request.args.get("mode", "easy")
|
||||
is_draft = variables.get("IS_DRAFT", "no") == "yes"
|
||||
is_draft = variables.pop("IS_DRAFT", "no") == "yes"
|
||||
|
||||
def update_service(variables: Dict[str, str], is_draft: bool, threaded: bool = False): # TODO: handle easy and raw modes
|
||||
def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode: str): # TODO: handle easy and raw modes
|
||||
wait_applying()
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
if service_exists:
|
||||
if service != "new":
|
||||
config = DB.get_config(methods=True, with_drafts=True, filtered_settings=list(variables.keys()), service=service)
|
||||
else:
|
||||
config = DB.get_config(methods=True, with_drafts=True, filtered_settings=list(variables.keys()))
|
||||
was_draft = config.get(f"{service}_IS_DRAFT", {"value": "no"})["value"] == "yes"
|
||||
config = DB.get_config(global_only=True, methods=True, filtered_settings=list(variables.keys()))
|
||||
was_draft = config.get("IS_DRAFT", {"value": "no"})["value"] == "yes"
|
||||
|
||||
old_server_name = variables.pop("OLD_SERVER_NAME", "")
|
||||
ignored_multiples = set()
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
for variable, value in variables.copy().items():
|
||||
if variable != "SERVER_NAME" and value == config.get(f"{service}_{variable}", {"value": None})["value"]:
|
||||
if (mode == "raw" or variable != "SERVER_NAME") and value == config.get(variable, {"value": None})["value"]:
|
||||
if match(r"^.+_\d+$", variable):
|
||||
ignored_multiples.add(variable)
|
||||
del variables[variable]
|
||||
|
||||
variables = BW_CONFIG.check_variables(variables, config, ignored_multiples=ignored_multiples, threaded=threaded)
|
||||
variables = BW_CONFIG.check_variables(variables, config, ignored_multiples=ignored_multiples, new=service == "new", threaded=True)
|
||||
|
||||
if was_draft == is_draft and not variables:
|
||||
content = f"The service {service} was not edited because no values were changed."
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": content, "type": "warning"})
|
||||
else:
|
||||
flash(content, "warning")
|
||||
DATA["TO_FLASH"].append({"content": content, "type": "warning"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
if "SERVER_NAME" not in variables:
|
||||
if service == "new":
|
||||
DATA["TO_FLASH"].append({"content": "The service was not created because the server name was not provided.", "type": "error"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
variables["SERVER_NAME"] = old_server_name
|
||||
|
||||
if service == "new":
|
||||
old_server_name = variables["SERVER_NAME"]
|
||||
|
||||
manage_bunkerweb(
|
||||
"services",
|
||||
variables,
|
||||
old_server_name,
|
||||
operation="edit" if service_exists else "new",
|
||||
operation="edit" if service != "new" else "new",
|
||||
is_draft=is_draft,
|
||||
was_draft=was_draft,
|
||||
threaded=threaded,
|
||||
threaded=True,
|
||||
)
|
||||
|
||||
DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True})
|
||||
Thread(target=update_service, args=(variables, is_draft, True)).start()
|
||||
Thread(target=update_service, args=(service, variables, is_draft, mode)).start()
|
||||
|
||||
if service == "new":
|
||||
service = variables["SERVER_NAME"].split(" ")[0]
|
||||
|
||||
arguments = {}
|
||||
if mode != "easy":
|
||||
|
|
@ -107,17 +175,32 @@ def services_service_page(service: str):
|
|||
service=service,
|
||||
)
|
||||
+ f"?{'&'.join([f'{k}={v}' for k, v in arguments.items()])}",
|
||||
message=f"Saving configuration for {'draft ' if is_draft else ''}service {service}",
|
||||
message=f"{'Saving' if service != 'new' else 'Creating'} configuration for {'draft ' if is_draft else ''}service {service}",
|
||||
)
|
||||
)
|
||||
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if not service_exists:
|
||||
db_config = DB.get_config(global_only=True, methods=True)
|
||||
return render_template("service_settings.html", config=db_config)
|
||||
|
||||
mode = request.args.get("mode", "easy")
|
||||
search_type = request.args.get("type", "all")
|
||||
if service == "new":
|
||||
clone = request.args.get("clone", "")
|
||||
if clone:
|
||||
db_config = DB.get_config(methods=True, with_drafts=True, service=clone)
|
||||
db_config["SERVER_NAME"]["value"] = ""
|
||||
return render_template(
|
||||
"service_settings.html",
|
||||
config=db_config,
|
||||
mode=mode,
|
||||
type=search_type,
|
||||
)
|
||||
|
||||
db_config = DB.get_config(global_only=True, methods=True)
|
||||
return render_template(
|
||||
"service_settings.html",
|
||||
config=db_config,
|
||||
mode=mode,
|
||||
type=search_type,
|
||||
)
|
||||
|
||||
db_config = DB.get_config(methods=True, with_drafts=True, service=service)
|
||||
return render_template(
|
||||
"service_settings.html",
|
||||
|
|
|
|||
|
|
@ -147,6 +147,10 @@
|
|||
* Custom
|
||||
******************************************************************************/
|
||||
|
||||
:root {
|
||||
--dt-row-selected: 29, 123, 167;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
padding: 0.35rem;
|
||||
font-size: 0.6rem;
|
||||
|
|
@ -280,11 +284,18 @@ td.highlight {
|
|||
color: var(--bs-bw-green) !important;
|
||||
}
|
||||
|
||||
.btn-bw-green {
|
||||
color: #fff;
|
||||
background-color: var(--bs-bw-green);
|
||||
border-color: var(--bs-bw-green);
|
||||
}
|
||||
|
||||
.btn-outline-bw-green {
|
||||
color: var(--bs-bw-green);
|
||||
border-color: var(--bs-bw-green);
|
||||
}
|
||||
|
||||
.btn-bw-green:hover,
|
||||
.btn-outline-bw-green:hover {
|
||||
color: #fff;
|
||||
background-color: var(--bs-bw-green);
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:root {
|
||||
--dt-row-selected: 29, 123, 167;
|
||||
}
|
||||
|
|
@ -258,6 +258,7 @@ $(document).ready(function () {
|
|||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
$(".dt-button-background").click();
|
||||
|
||||
const instances = getSelectedInstances();
|
||||
if (instances.length === 0) {
|
||||
|
|
|
|||
359
src/ui/app/static/js/pages/services.js
Normal file
359
src/ui/app/static/js/pages/services.js
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
$(document).ready(function () {
|
||||
var toastNum = 0;
|
||||
var actionLock = false;
|
||||
const serviceNumber = parseInt($("#services_number").val());
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
};
|
||||
|
||||
if (serviceNumber > 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))",
|
||||
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_services",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "excel",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>Excel',
|
||||
filename: "bw_services",
|
||||
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: "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>',
|
||||
},
|
||||
{
|
||||
extend: "convert_services",
|
||||
text: '<span class="tf-icons bx bx-file-blank bx-18px me-2"></span>Convert to<span class="d-none d-md-inline"> draft</span>',
|
||||
},
|
||||
{
|
||||
extend: "delete_services",
|
||||
className: "text-danger",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
extend: "create_service",
|
||||
},
|
||||
];
|
||||
|
||||
$(document).on("hidden.bs.toast", ".toast", function (event) {
|
||||
if (event.target.id.startsWith("feedback-toast")) {
|
||||
setTimeout(() => {
|
||||
$(this).remove();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
$("#modal-delete-services").on("hidden.bs.modal", function () {
|
||||
$("#selected-services-delete").empty();
|
||||
$("#selected-services-input-delete").val("");
|
||||
});
|
||||
|
||||
$("#modal-convert-services").on("hidden.bs.modal", function () {
|
||||
$("#selected-services-convert").empty();
|
||||
$("#selected-services-input-convert").val("");
|
||||
});
|
||||
|
||||
const getSelectedservices = () => {
|
||||
const services = [];
|
||||
$("tr.selected").each(function () {
|
||||
services.push($(this).find("td:eq(1)").find("a").text().trim());
|
||||
});
|
||||
return services;
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.create_service = {
|
||||
text: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Create<span class="d-none d-md-inline"> new service</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.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) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
$(".dt-button-background").click();
|
||||
const convertionType = $(node).text().trim().split(" ")[2];
|
||||
|
||||
const services = getSelectedservices();
|
||||
if (services.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredServices = services.filter(function (service) {
|
||||
const serviceType = $(`#type-${service.replace(/\./g, "-")}`);
|
||||
return serviceType.data("value") !== convertionType;
|
||||
});
|
||||
if (filteredServices.length === 0) {
|
||||
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("Conversion failed");
|
||||
feedbackToast
|
||||
.find("div.toast-body")
|
||||
.text("The selected services are already in the desired state.");
|
||||
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
feedbackToast.toast("show");
|
||||
actionLock = false;
|
||||
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();
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete_services = {
|
||||
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 services = getSelectedservices();
|
||||
if (services.length === 0) {
|
||||
actionLock = false;
|
||||
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();
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
};
|
||||
|
||||
const services_table = new DataTable("#services", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
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: [[5, "desc"]],
|
||||
autoFill: false,
|
||||
colReorder: true,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:first-child",
|
||||
headerCheckbox: false,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ services",
|
||||
infoEmpty: "No services available",
|
||||
infoFiltered: "(filtered from _MAX_ total services)",
|
||||
lengthMenu: "Display _MENU_ services",
|
||||
zeroRecords: "No matching services found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d services",
|
||||
0: "No services selected",
|
||||
1: "Selected 1 service",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#services_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
});
|
||||
|
||||
services_table.on("mouseenter", "td", function () {
|
||||
const rowIdx = services_table.cell(this).index().row;
|
||||
|
||||
services_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
services_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (services_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
services_table.on("mouseleave", "td", function () {
|
||||
services_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
|
||||
services_table.rows({ page: "current" }).select();
|
||||
} else {
|
||||
// Deselect all rows on the current page
|
||||
services_table.rows({ page: "current" }).deselect();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
$(document).ready(() => {
|
||||
var toastNum = 0;
|
||||
let currentPlugin = "general";
|
||||
let currentMode = "easy";
|
||||
let currentType = "all";
|
||||
let currentMode = $("#selected-mode").val();
|
||||
let currentType = $("#selected-type").val();
|
||||
|
||||
const $serviceMethodInput = $("#service-method");
|
||||
const $pluginSearch = $("#plugin-search");
|
||||
const $pluginTypeSelect = $("#plugin-type-select");
|
||||
const $pluginKeywordSearch = $("#plugin-keyword-search");
|
||||
|
|
@ -107,6 +109,124 @@ $(document).ready(() => {
|
|||
}
|
||||
};
|
||||
|
||||
const getFormFromSettings = () => {
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: window.location.href,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
// Helper function to append hidden inputs
|
||||
const appendHiddenInput = (form, name, value) => {
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: name,
|
||||
value: $("<div>").text(value).html(), // Sanitize the value
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Handle missing CSRF token gracefully
|
||||
const csrfToken = $("#csrf_token").val() || "";
|
||||
appendHiddenInput(form, "csrf_token", csrfToken);
|
||||
|
||||
// TODO: support easy mode
|
||||
if (currentMode === undefined || currentMode === "advanced") {
|
||||
$("div[id^='navs-plugins-']")
|
||||
.find("input, select")
|
||||
.each(function () {
|
||||
const $this = $(this);
|
||||
const settingName = $this.attr("name");
|
||||
const settingType = $this.attr("type");
|
||||
const originalValue = $this.data("original");
|
||||
let settingValue = $this.val();
|
||||
|
||||
if ($this.is("select")) {
|
||||
settingValue = $this.val();
|
||||
} else if (settingType === "checkbox") {
|
||||
settingValue = $this.is(":checked") ? "yes" : "no";
|
||||
}
|
||||
|
||||
if (settingValue == originalValue) return;
|
||||
|
||||
appendHiddenInput(form, settingName, settingValue);
|
||||
});
|
||||
|
||||
const $draftInput = $("#is-draft");
|
||||
if ($draftInput.length) {
|
||||
appendHiddenInput(form, "IS_DRAFT", $draftInput.val());
|
||||
}
|
||||
} else if (currentMode === "raw") {
|
||||
// Helper function to parse configuration strings into an object
|
||||
const parseConfig = (selector) => {
|
||||
const rawConfig = $(selector).val();
|
||||
if (!rawConfig) return {};
|
||||
return rawConfig
|
||||
.trim()
|
||||
.split("\n")
|
||||
.reduce((acc, line) => {
|
||||
const [key, value] = line.split("=");
|
||||
if (key && value !== undefined) {
|
||||
acc[key.trim()] = value.trim();
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// Parse original and default configurations
|
||||
const configOriginals = parseConfig("#raw-config-originals");
|
||||
const configDefaults = parseConfig("#raw-config-defaults");
|
||||
|
||||
// Sets to keep track of processed keys
|
||||
const formKeys = new Set();
|
||||
const skippedKeys = new Set();
|
||||
|
||||
// Process the current configuration
|
||||
const $rawConfig = $("#raw-config");
|
||||
if ($rawConfig.length) {
|
||||
const configLines = $rawConfig
|
||||
.val()
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"));
|
||||
|
||||
configLines.forEach((line) => {
|
||||
const [key, value] = line.split("=").map((str) => str.trim());
|
||||
if (!key || value === undefined) {
|
||||
console.warn(`Skipping malformed line: ${line}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip unchanged values except for 'IS_DRAFT'
|
||||
if (key !== "IS_DRAFT" && configOriginals[key] === value) {
|
||||
skippedKeys.add(key);
|
||||
return;
|
||||
}
|
||||
|
||||
appendHiddenInput(form, key, value);
|
||||
formKeys.add(key);
|
||||
});
|
||||
}
|
||||
|
||||
// Append default values if they are not already in the form and not skipped
|
||||
Object.entries(configDefaults).forEach(([key, value]) => {
|
||||
if (!formKeys.has(key) && !skippedKeys.has(key)) {
|
||||
appendHiddenInput(form, key, value);
|
||||
formKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Append 'OLD_SERVER_NAME' if it exists
|
||||
const $oldServerName = $("#old-server-name");
|
||||
if ($oldServerName.length) {
|
||||
appendHiddenInput(form, "OLD_SERVER_NAME", $oldServerName.val());
|
||||
}
|
||||
}
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return (...args) => {
|
||||
|
|
@ -465,57 +585,59 @@ $(document).ready(() => {
|
|||
});
|
||||
});
|
||||
|
||||
$("#save-settings").on("click", function () {
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: window.location.href,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
$(".save-settings").on("click", function () {
|
||||
const form = getFormFromSettings();
|
||||
// TODO: support easy mode
|
||||
let minSettings = 4;
|
||||
if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2;
|
||||
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
}),
|
||||
);
|
||||
$("div[id^='navs-plugins-']")
|
||||
.find("input, select")
|
||||
.each(function () {
|
||||
const settingName = $(this).attr("name");
|
||||
const settingType = $(this).attr("type");
|
||||
const originalValue = $(this).data("original");
|
||||
var settingValue = $(this).val();
|
||||
const draftInput = $("#is-draft");
|
||||
const wasDraft = draftInput.data("original") === "yes";
|
||||
let isDraft = draftInput.val() === "yes";
|
||||
if (currentMode === "raw")
|
||||
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
|
||||
|
||||
if ($(this).is("select")) {
|
||||
settingValue = $(this).find("option:selected").val();
|
||||
} else if (settingType === "checkbox") {
|
||||
settingValue = $(this).prop("checked") ? "yes" : "no";
|
||||
}
|
||||
|
||||
if (
|
||||
$(this).attr("id") &&
|
||||
!$(this).attr("id").startsWith("multiple-") &&
|
||||
settingValue == originalValue
|
||||
)
|
||||
return;
|
||||
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: settingName,
|
||||
value: settingValue,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
if (form.children().length < 2) {
|
||||
if (form.children().length < minSettings && isDraft === wasDraft) {
|
||||
alert("No changes detected.");
|
||||
return;
|
||||
}
|
||||
$(window).off("beforeunload");
|
||||
form.appendTo("body").submit();
|
||||
});
|
||||
|
||||
$("#toggle-draft").on("click", function () {
|
||||
const draftInput = $("#is-draft");
|
||||
const isDraft = draftInput.val() === "yes";
|
||||
|
||||
draftInput.val(isDraft ? "no" : "yes");
|
||||
$(this).html(
|
||||
`<i class="bx bx-sm bx-${
|
||||
isDraft ? "globe" : "file-blank"
|
||||
} bx-sm"></i> ${isDraft ? "Online" : "Draft"}`,
|
||||
);
|
||||
});
|
||||
|
||||
$(".copy-settings").on("click", function () {
|
||||
const config = $("#raw-config").val();
|
||||
|
||||
// Use the Clipboard API
|
||||
navigator.clipboard
|
||||
.writeText(config)
|
||||
.then(() => {
|
||||
// Show tooltip
|
||||
const button = $(this);
|
||||
button.attr("data-bs-original-title", "Copied!").tooltip("show");
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
button.tooltip("hide").attr("data-bs-original-title", "");
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy text: ", err);
|
||||
});
|
||||
});
|
||||
|
||||
$('div[id^="multiple-"]')
|
||||
.filter(function () {
|
||||
return /^multiple-.*-\d+$/.test($(this).attr("id"));
|
||||
|
|
@ -602,4 +724,65 @@ $(document).ready(() => {
|
|||
}
|
||||
|
||||
$pluginTypeSelect.trigger("change");
|
||||
|
||||
if (currentMode === "advanced") {
|
||||
const serverNameSetting = $("#setting-general-server-name");
|
||||
if (!serverNameSetting.val()) {
|
||||
if (currentType !== "all") {
|
||||
currentType = "all";
|
||||
$pluginTypeSelect.val("all");
|
||||
} else
|
||||
$(`button[data-bs-target="#navs-plugins-${currentPlugin}"]`).tab(
|
||||
"show",
|
||||
);
|
||||
|
||||
if (currentPlugin !== "general") {
|
||||
$(`button[data-bs-target="#navs-plugins-general"]`).tab("show");
|
||||
}
|
||||
|
||||
updateUrlParams({ type: null });
|
||||
|
||||
highlightSettings(serverNameSetting.closest(".col-12"));
|
||||
serverNameSetting.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if ($serviceMethodInput.length) {
|
||||
if ($serviceMethodInput.val() === "autoconf") {
|
||||
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("The service method is autoconf.");
|
||||
feedbackToast
|
||||
.find("div.toast-body")
|
||||
.html(
|
||||
"<p>As the service method is set to autoconf, the configuration is locked. <div class='fw-bolder'>Any changes made will not be saved.</div><div class='fst-italic'>This is to prevent conflicts with the autoconf and the web UI.</div></p>",
|
||||
);
|
||||
feedbackToast.attr("data-bs-autohide", "false");
|
||||
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
feedbackToast.toast("show");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).on("beforeunload", function (e) {
|
||||
const form = getFormFromSettings();
|
||||
// TODO: support easy mode
|
||||
let minSettings = 4;
|
||||
if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2;
|
||||
|
||||
const draftInput = $("#is-draft");
|
||||
const wasDraft = draftInput.data("original") === "yes";
|
||||
let isDraft = draftInput.val() === "yes";
|
||||
if (currentMode === "raw")
|
||||
isDraft = form.find("input[name='IS_DRAFT']").val() === "yes";
|
||||
|
||||
if (form.children().length < minSettings && isDraft === wasDraft) return;
|
||||
|
||||
// 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
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@
|
|||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='fonts/boxicons.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% if current_endpoint in ("instances", "services") %}
|
||||
<!-- Datatables -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/datatables/datatables.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% endif %}
|
||||
<!-- Core CSS -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/core.css') }}"
|
||||
|
|
@ -43,13 +49,6 @@
|
|||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/apexcharts/apexcharts.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% elif current_endpoint == "instances" %}
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/datatables/datatables.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/pages/instances.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% endif %}
|
||||
<!-- Page CSS -->
|
||||
<!-- Page -->
|
||||
|
|
@ -90,7 +89,7 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='libs/purify/purify.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% if current_endpoint == "instances" %}
|
||||
{% if current_endpoint in ("instances", "services") %}
|
||||
<script src="{{ url_for('static', filename='libs/datatables/datatables.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
|
|
@ -125,6 +124,9 @@
|
|||
{% elif current_endpoint == "instances" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/instances.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "services" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/services.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
<script async defer src="{{ url_for('static', filename='js/buttons.js') }}"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
{% set blacklisted_settings = get_blacklisted_settings(true) %}
|
||||
{% set service_method = "ui" %}
|
||||
{% include "models/plugins_settings.html" %}
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
type="checkbox"
|
||||
role="switch"
|
||||
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
data-original="{{ setting_value }}"
|
||||
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
|
||||
data-default="{{ setting_default }}"
|
||||
{% if setting_value == "yes" %}checked{% endif %}
|
||||
{% if disabled %}disabled{% endif %}>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
pattern="{{ setting_data['regex'] }}"
|
||||
value="{{ setting_value }}"
|
||||
data-original="{{ setting_value }}"
|
||||
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
|
||||
data-default="{{ setting_default }}"
|
||||
{% if disabled %}disabled{% endif %}>
|
||||
<label for="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
{% set blacklisted_settings = get_blacklisted_settings(current_endpoint == "global-config") %}
|
||||
<input type="hidden"
|
||||
id="selected-type"
|
||||
name="selected_type"
|
||||
value="{{ type }}">
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}">
|
||||
<div class="card p-1 mb-4 sticky-card">
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}">
|
||||
<div class="d-flex flex-wrap justify-content-around align-items-center">
|
||||
<div class="dropdown btn-group">
|
||||
<button id="select-plugin"
|
||||
|
|
@ -58,12 +61,38 @@
|
|||
<label for="plugin-type-select">Type</label>
|
||||
</div>
|
||||
</div>
|
||||
<button id="save-settings"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-bw-green">
|
||||
<i class="bx bx-save bx-sm"></i>
|
||||
<span class="d-none d-md-inline"> Save</span>
|
||||
</button>
|
||||
<div class="d-flex justify-content-center">
|
||||
{% if current_endpoint != "global-config" %}
|
||||
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" title="The draft mode can only be toggled on UI created services"{% endif %}>
|
||||
{% set is_draft = config.get('IS_DRAFT', {}).get('value', 'no') %}
|
||||
<input type="hidden"
|
||||
id="is-draft"
|
||||
name="IS_DRAFT"
|
||||
value="{{ is_draft }}"
|
||||
data-original="{% if current_endpoint != 'new' %}{{ is_draft }}{% else %}no{% endif %}"
|
||||
data-default="no">
|
||||
<button id="toggle-draft"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary me-3 {% if current_endpoint != 'new' and service_method != 'ui' %}disabled{% endif %}">
|
||||
<i class="bx bx-sm bx-{% if is_draft == 'yes' %}file-blank{% else %}globe{% endif %}"></i>
|
||||
<span class="d-none d-md-inline">
|
||||
{% if is_draft == 'yes' %}
|
||||
Draft
|
||||
{% else %}
|
||||
Online
|
||||
{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" %}disabled{% endif %}">
|
||||
<i class="bx bx-save bx-sm"></i>
|
||||
<span class="d-none d-md-inline"> Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set plugin_types = {
|
||||
|
|
@ -88,26 +117,35 @@
|
|||
role="tabpanel"
|
||||
aria-labelledby="navs-plugins-{{ plugin['id'] }}-tab"
|
||||
data-type="{{ plugin['type'] }}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="pt-1">
|
||||
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
|
||||
{{ plugin["name"] }} - v{{ plugin["version"] }} - {{ plugin_types[plugin["type"]].get('icon', '<img src="' + pro_diamond_url + '"
|
||||
<div class="card-header d-flex justify-content-between align-items-center mw-100">
|
||||
<div class="pt-1 flex-grow-1 me-2" style="min-width: 0;">
|
||||
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
|
||||
{{ plugin["name"] }} - v{{ plugin["version"] }} - {{ plugin_types[plugin["type"]].get('icon', '<img src="' + pro_diamond_url + '"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">') |safe }}
|
||||
</h5>
|
||||
<p class="card-subtitle text-muted mt-2">{{ plugin["description"] }}</p>
|
||||
</h5>
|
||||
<p class="card-subtitle text-muted text-truncate mt-2">{{ plugin["description"] }}</p>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="d-flex flex-grow-0 flex-shrink-0 justify-content-end align-items-center">
|
||||
<a href="https://docs.bunkerweb.io/latest/quickstart-guide/#protect-udptcp-applications"
|
||||
class="btn btn-sm btn-{% if plugin['stream'] == 'yes' %}bw-green{% elif plugin['stream'] == 'partial' %}warning{% else %}danger{% endif %} rounded-pill"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="{% if plugin['stream'] != 'no' %}Supports{% else %}Doesn't support{% endif %} STREAM mode{% if plugin['stream'] == 'partial' %} partially{% endif %}">
|
||||
<i class="bx bx-{% if plugin['stream'] == 'yes' %}badge-check{% elif plugin['stream'] == 'partial' %}message-square-detail{% else %}no-entry{% endif %}"></i> STREAM
|
||||
</a>
|
||||
<a href="{% if plugin['type'] == 'core' %}https://docs.bunkerweb.io/latest/settings/?utm_campaign=self&utm_source=ui#{% if plugin['id'] == 'general' %}global-settings{% else %}{{ plugin['id'] }}{% endif %}{% else %}https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=ui{% endif %}"
|
||||
class="btn btn-sm btn-outline-primary rounded-pill"
|
||||
class="btn btn-sm btn-primary rounded-pill ms-2"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class="bx bx-link"></i> More info
|
||||
</a>
|
||||
{% if plugin["page"] %}
|
||||
<a href="{{ url_for('plugins') }}/{{ plugin['id'] }}"
|
||||
class="btn btn-sm btn-outline-primary rounded-pill ms-2">
|
||||
<a href="{{ url_for("plugins") }}/{{ plugin['id'] }}"
|
||||
class="btn btn-sm btn-primary rounded-pill ms-2">
|
||||
<i class="bx bxs-file-html"></i> Custom page
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
@ -116,11 +154,21 @@
|
|||
<div class="card-body row pb-0">
|
||||
{% for setting, setting_data in filtered_settings.items() if not setting_data.get('multiple', false) and setting not in blacklisted_settings and (not service_endpoint or setting_data['context'] == "multisite") %}
|
||||
{% set setting_config = config.get(setting, {}) %}
|
||||
{% set setting_value = setting_config.get("value", setting_data["default"]) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_default = setting_data.get("default", "") %}
|
||||
{% set setting_value = setting_config.get("value", setting_default) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_template = setting_config.get("template", "") %}
|
||||
{% set disabled = setting_method not in ('ui', 'default') %}
|
||||
{% if current_endpoint == "new" %}
|
||||
{% set disabled = false %}
|
||||
{% if setting == "SERVER_NAME" %}
|
||||
{% set setting_value = "" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if service_method == "autoconf" %}
|
||||
{% set setting_method = "autoconf" %}
|
||||
{% set disabled = true %}
|
||||
{% endif %}
|
||||
<div class="col-12 col-sm-6 col-lg-4 pb-3"
|
||||
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
|
@ -164,7 +212,10 @@
|
|||
{% include "models/checkbox_setting.html" %}
|
||||
{% else %}
|
||||
{% if setting == "SERVER_NAME" and current_endpoint != "global-config" %}
|
||||
<input type="hidden" name="OLD_SERVER_NAME" value="{{ setting_value }}">
|
||||
<input type="hidden"
|
||||
id="old-server-name"
|
||||
name="OLD_SERVER_NAME"
|
||||
value="{{ setting_value }}">
|
||||
{% endif %}
|
||||
{% include "models/input_setting.html" %}
|
||||
{% endif %}
|
||||
|
|
@ -227,11 +278,15 @@
|
|||
<div class="row mt-2 pt-2">
|
||||
{% for setting, setting_data in settings.items() if setting not in blacklisted_settings %}
|
||||
{% set setting_config = config.get(setting, {}) %}
|
||||
{% set setting_value = setting_config.get("value", setting_data["default"]) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_default = setting_data.get("default", "") %}
|
||||
{% set setting_value = setting_config.get("value", setting_default) %}
|
||||
{% set setting_method = setting_config.get("method", "default") %}
|
||||
{% set setting_template = setting_config.get("template", "") %}
|
||||
{% set disabled = setting_method not in ('ui', 'default') %}
|
||||
{% set disabled = current_endpoint != "new" and setting_method not in ('ui', 'default') %}
|
||||
{% if service_method == "autoconf" %}
|
||||
{% set setting_method = "autoconf" %}
|
||||
{% set disabled = true %}
|
||||
{% endif %}
|
||||
<div class="col-12{% if multiple_settings %} col-md-6{% endif %}{% if settings|length > 2 and not multiple_multiples %} col-lg-4{% endif %} pb-2"
|
||||
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
|
|
|||
0
src/ui/app/templates/models/plugins_settings_easy.html
Normal file
0
src/ui/app/templates/models/plugins_settings_easy.html
Normal file
61
src/ui/app/templates/models/plugins_settings_raw.html
Normal file
61
src/ui/app/templates/models/plugins_settings_raw.html
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}">
|
||||
<div class="position-absolute top-0 end-0 m-3"
|
||||
style="z-index: 1000">
|
||||
<div class="d-flex flex-wrap justify-content-center align-items-center">
|
||||
<div class="card p-1 me-2">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-secondary copy-settings">
|
||||
<i class="bx bx-copy-alt bx-xs"></i>
|
||||
<span class="d-none d-md-inline"> Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card p-1"{% if service_method == "autoconf" %} data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
|
||||
<!-- Save button container -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" %}disabled{% endif %}">
|
||||
<i class="bx bx-save bx-xs"></i>
|
||||
<span class="d-none d-md-inline"> Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-primary p-1 d-flex flex-column"
|
||||
{% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ service_method }}"{% endif %}>
|
||||
{% set config_lines = ["IS_DRAFT=" + config.get('IS_DRAFT', {}).get('value', 'no')] %}
|
||||
{% set default_settings = ["IS_DRAFT=no"] %}
|
||||
{% for plugin in plugins %}
|
||||
{% set filtered_settings = get_filtered_settings(plugin["settings"], current_endpoint == "global-config") %}
|
||||
{% if filtered_settings %}
|
||||
{% for setting, setting_data in filtered_settings.items() if setting not in blacklisted_settings %}
|
||||
{% set setting_config = config.get(setting, {}) %}
|
||||
{% set setting_default = setting_data.get("default", "") %}
|
||||
{% set setting_value = setting_config.get("value", setting_default) %}
|
||||
{% if setting_value != setting_default %}
|
||||
{% if config_lines.append(setting + "=" + setting_value) %}{% endif %}
|
||||
{% if default_settings.append(setting + "=" + setting_default) %}{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- Join the configuration lines with newlines -->
|
||||
{% set raw_config = config_lines | join('\r\n') %}
|
||||
<input type="hidden"
|
||||
id="raw-config-originals"
|
||||
name="raw-config"
|
||||
value="{{ raw_config }}">
|
||||
<input type="hidden"
|
||||
id="raw-config-defaults"
|
||||
name="raw-config"
|
||||
value="{{ default_settings | join('\r\n') }}">
|
||||
<div class="form-floating">
|
||||
<textarea class="form-control w-100 h-100 text-white"
|
||||
rows="35"
|
||||
id="raw-config"
|
||||
aria-label="Raw configuration"
|
||||
{% if service_method == "autoconf" %}disabled{% endif %}>{{ raw_config|safe }}</textarea>
|
||||
<label for="raw-config" class="text-white">Raw configuration</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
name="{{ setting }}"
|
||||
class="form-select"
|
||||
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
|
||||
data-original="{{ setting_value }}"
|
||||
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
|
||||
data-default="{{ setting_default }}"
|
||||
{% if disabled %}disabled{% endif %}>
|
||||
{% for option in setting_data["select"] %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
{% set blacklisted_settings = get_blacklisted_settings() %}
|
||||
{% set service_method = "ui" %}
|
||||
{% if current_endpoint != "global-config" %}
|
||||
{% set service_method = config.get("SERVER_NAME", {"method": "ui"})["method"] %}
|
||||
<input type="hidden"
|
||||
id="selected-mode"
|
||||
name="selected_mode"
|
||||
value="{{ mode }}">
|
||||
{% endif %}
|
||||
<input type="hidden"
|
||||
id="service-method"
|
||||
name="service_method"
|
||||
value="{{ service_method }}">
|
||||
<div class="tab-content p-0 position-relative">
|
||||
<div class="tab-pane fade{% if mode == 'easy' %} show active{% endif %}"
|
||||
id="navs-modes-easy"
|
||||
|
|
@ -8,9 +21,26 @@
|
|||
<div class="tab-pane fade{% if mode == 'advanced' %} show active{% endif %}"
|
||||
id="navs-modes-advanced"
|
||||
role="tabpanel">{% include "models/plugins_settings.html" %}</div>
|
||||
<div class="tab-pane fade{% if mode == 'raw' %} show active{% endif %}"
|
||||
<div class="tab-pane position-relative fade{% if mode == 'raw' %} show active{% endif %}"
|
||||
id="navs-modes-raw"
|
||||
role="tabpanel">TODO: Add Raw mode</div>
|
||||
role="tabpanel">{% include "models/plugins_settings_raw.html" %}</div>
|
||||
</div>
|
||||
<div id="feedback-toast"
|
||||
class="bs-toast toast fade"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
data-bs-autohide="true">
|
||||
<div class="toast-header">
|
||||
<i class="d-block w-px-20 h-auto rounded me-2 tf-icons bx bx-bell"></i>
|
||||
<span class="fw-medium me-auto">BunkerWeb Forever</span>
|
||||
<small class="text-body-secondary">just now</small>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="toast"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">If you read this, it means that you're curious 👀</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
|
|||
154
src/ui/app/templates/services.html
Normal file
154
src/ui/app/templates/services.html
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="services_number" value="{{ services|length }}" />
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="services" 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 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>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
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 date and time when the Service was created">Created</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The date and time when the Service was last updated">Last Update</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a 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> {{ service["id"] }}</a>
|
||||
</td>
|
||||
<td id="method-{{ service['id'].replace('.', '-') }}">{{ service["method"] }}</td>
|
||||
<td>
|
||||
{% if service['is_draft'] %}
|
||||
<span id="type-{{ service['id'].replace('.', '-') }}"
|
||||
data-value="draft"
|
||||
class="badge rounded-pill bg-label-secondary">Draft</span>
|
||||
{% else %}
|
||||
<span id="type-{{ service['id'].replace('.', '-') }}"
|
||||
data-value="online"
|
||||
class="badge rounded-pill bg-label-primary">Online</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="feedback-toast"
|
||||
class="bs-toast toast fade"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
data-bs-autohide="true">
|
||||
<div class="toast-header">
|
||||
<i class="d-block w-px-20 h-auto rounded me-2 tf-icons bx bx-bell"></i>
|
||||
<span class="fw-medium me-auto">BunkerWeb Forever</span>
|
||||
<small class="text-body-secondary">just now</small>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="toast"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">If you read this, it means that you're curious 👀</div>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="modal-convert-services"
|
||||
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">Convert services to Draft</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for("services") }}/convert" method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" id="convertion-type" name="convert_to" value="draft" />
|
||||
<input type="hidden"
|
||||
id="selected-services-input-convert"
|
||||
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>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-secondary me-2">Convert services</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="modal-delete-services"
|
||||
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">Delete services</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for("services") }}/delete" method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden"
|
||||
id="selected-services-input-delete"
|
||||
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>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Delete services</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
Loading…
Reference in a new issue