Add services page to web UI and finish raw mode for service settings v1

This commit is contained in:
Théophile Diot 2024-09-13 10:34:16 +02:00
parent 37cdd322af
commit 27152f0977
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
19 changed files with 1108 additions and 131 deletions

View file

@ -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:

View file

@ -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")

View file

@ -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

View file

@ -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",

View file

@ -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);

View file

@ -1,3 +0,0 @@
:root {
--dt-row-selected: 29, 123, 167;
}

View file

@ -258,6 +258,7 @@ $(document).ready(function () {
return;
}
actionLock = true;
$(".dt-button-background").click();
const instances = getSelectedInstances();
if (instances.length === 0) {

View 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();
}
});
});

View file

@ -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>&nbsp;${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
});
});

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}>

View file

@ -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 }}">

View file

@ -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">&nbsp;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">&nbsp;
{% 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">&nbsp;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"] }}&nbsp;&nbsp;v{{ plugin["version"] }}&nbsp;&nbsp;{{ 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"] }}&nbsp;&nbsp;v{{ plugin["version"] }}&nbsp;&nbsp;{{ 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>&nbsp;STREAM
</a>
<a href="{% if plugin['type'] == 'core' %}https://docs.bunkerweb.io/latest/settings/?utm_campaign=self&utm_source=ui#{% if plugin['id'] == 'general' %}global-settings{% else %}{{ plugin['id'] }}{% endif %}{% else %}https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=ui{% endif %}"
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>&nbsp;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>&nbsp;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">

View 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">&nbsp;Copy</span>
</button>
</div>
<div class="card p-1"{% if service_method == "autoconf" %} data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
<!-- 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">&nbsp;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>

View file

@ -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"] %}

View file

@ -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 %}

View 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>&nbsp;{{ 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 %}