mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Finished v1 of instances page in web UI
This commit is contained in:
parent
bff3567a8f
commit
aa117403bd
15 changed files with 614 additions and 256 deletions
|
|
@ -3154,6 +3154,34 @@ class Database:
|
|||
|
||||
return ""
|
||||
|
||||
def delete_instances(self, hostnames: List[str], changed: Optional[bool] = True) -> str:
|
||||
"""Delete instances."""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
db_instances = session.query(Instances).filter(Instances.hostname.in_(hostnames)).all()
|
||||
|
||||
if not db_instances:
|
||||
return "No instances found to delete."
|
||||
|
||||
for db_instance in db_instances:
|
||||
session.delete(db_instance)
|
||||
|
||||
if changed:
|
||||
with suppress(ProgrammingError, OperationalError):
|
||||
metadata = session.query(Metadata).get(1)
|
||||
if metadata is not None:
|
||||
metadata.instances_changed = True
|
||||
metadata.last_instances_change = datetime.now().astimezone()
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
return f"An error occurred while deleting the instances {', '.join(hostnames)}.\n{e}"
|
||||
|
||||
return ""
|
||||
|
||||
def delete_instance(self, hostname: str, changed: Optional[bool] = True) -> str:
|
||||
"""Delete instance."""
|
||||
with self._db_session() as session:
|
||||
|
|
@ -3236,7 +3264,8 @@ class Database:
|
|||
return f"Instance {hostname} does not exist, will not be updated."
|
||||
|
||||
db_instance.status = status
|
||||
db_instance.last_seen = datetime.now().astimezone()
|
||||
if status != "down":
|
||||
db_instance.last_seen = datetime.now().astimezone()
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class ApiCaller:
|
|||
with ThreadPoolExecutor() as executor:
|
||||
future_to_api = {executor.submit(send_request, api, deepcopy(files) if files else None): api for api in self.apis}
|
||||
for future in as_completed(future_to_api):
|
||||
api = future_to_api[future]
|
||||
try:
|
||||
api, sent, err, status, resp = future.result()
|
||||
if not sent:
|
||||
|
|
|
|||
|
|
@ -811,13 +811,13 @@ if __name__ == "__main__":
|
|||
|
||||
success, responses = SCHEDULER.send_to_apis("POST", "/reload", response=True)
|
||||
if not success:
|
||||
reachable = False
|
||||
LOGGER.debug(f"Error while reloading all bunkerweb instances: {responses}")
|
||||
|
||||
reachable = False
|
||||
for db_instance in SCHEDULER.db.get_instances():
|
||||
status = responses.get(db_instance["hostname"], {"status": "down"}).get("status", "down")
|
||||
if status == "success":
|
||||
reachable = True
|
||||
success = True
|
||||
ret = SCHEDULER.db.update_instance(db_instance["hostname"], "up" if status == "success" else "down")
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't update instance {db_instance['hostname']} status to down in the database: {ret}")
|
||||
|
|
|
|||
|
|
@ -69,72 +69,72 @@ class Instance:
|
|||
try:
|
||||
result = self.apiCaller.send_to_apis("POST", "/reload")[0]
|
||||
except BaseException as e:
|
||||
return f"Can't reload {self.hostname}: {e}"
|
||||
return f"Can't reload instance {self.hostname}: {e}"
|
||||
|
||||
if result:
|
||||
return f"Instance {self.hostname} has been reloaded."
|
||||
return f"Can't reload {self.hostname}"
|
||||
return f"Can't reload instance {self.hostname}"
|
||||
|
||||
def start(self) -> str:
|
||||
raise NotImplementedError("Method not implemented yet")
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("POST", "/start")[0]
|
||||
except BaseException as e:
|
||||
return f"Can't start {self.hostname}: {e}"
|
||||
return f"Can't start instance {self.hostname}: {e}"
|
||||
|
||||
if result:
|
||||
return f"Instance {self.hostname} has been started."
|
||||
return f"Can't start {self.hostname}"
|
||||
return f"Can't start instance {self.hostname}"
|
||||
|
||||
def stop(self) -> str:
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("POST", "/stop")[0]
|
||||
except BaseException as e:
|
||||
return f"Can't stop {self.hostname}: {e}"
|
||||
return f"Can't stop instance {self.hostname}: {e}"
|
||||
|
||||
if result:
|
||||
return f"Instance {self.hostname} has been stopped."
|
||||
return f"Can't stop {self.hostname}"
|
||||
return f"Can't stop instance {self.hostname}"
|
||||
|
||||
def restart(self) -> str:
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("POST", "/restart")[0]
|
||||
except BaseException as e:
|
||||
return f"Can't restart {self.hostname}: {e}"
|
||||
return f"Can't restart instance {self.hostname}: {e}"
|
||||
|
||||
if result:
|
||||
return f"Instance {self.hostname} has been restarted."
|
||||
return f"Can't restart {self.hostname}"
|
||||
return f"Can't restart instance {self.hostname}"
|
||||
|
||||
def ban(self, ip: str, exp: float, reason: str) -> str:
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("POST", "/ban", data={"ip": ip, "exp": exp, "reason": reason})[0]
|
||||
except BaseException as e:
|
||||
return f"Can't ban {ip} on {self.hostname}: {e}"
|
||||
return f"Can't ban {ip} on instance {self.hostname}: {e}"
|
||||
|
||||
if result:
|
||||
return f"IP {ip} has been banned on {self.hostname} for {exp} seconds{f' with reason: {reason}' if reason else ''}."
|
||||
return f"Can't ban {ip} on {self.hostname}"
|
||||
return f"IP {ip} has been banned on instance {self.hostname} for {exp} seconds{f' with reason: {reason}' if reason else ''}."
|
||||
return f"Can't ban {ip} on instance {self.hostname}"
|
||||
|
||||
def unban(self, ip: str) -> str:
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("POST", "/unban", data={"ip": ip})[0]
|
||||
except BaseException as e:
|
||||
return f"Can't unban {ip} on {self.hostname}: {e}"
|
||||
return f"Can't unban {ip} on instance {self.hostname}: {e}"
|
||||
|
||||
if result:
|
||||
return f"IP {ip} has been unbanned on {self.hostname}."
|
||||
return f"Can't unban {ip} on {self.hostname}"
|
||||
return f"IP {ip} has been unbanned on instance {self.hostname}."
|
||||
return f"Can't unban {ip} on instance {self.hostname}"
|
||||
|
||||
def bans(self) -> Tuple[str, dict[str, Any]]:
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("GET", "/bans", response=True)
|
||||
except BaseException as e:
|
||||
return f"Can't get bans from {self.hostname}: {e}", result[1]
|
||||
return f"Can't get bans from instance {self.hostname}: {e}", result[1]
|
||||
|
||||
if result[0]:
|
||||
return "", result[1]
|
||||
return f"Can't get bans from {self.hostname}", result[1]
|
||||
return f"Can't get bans from instance {self.hostname}", result[1]
|
||||
|
||||
def reports(self) -> Tuple[bool, dict[str, Any]]:
|
||||
return self.apiCaller.send_to_apis("GET", "/metrics/requests", response=True)
|
||||
|
|
@ -145,16 +145,16 @@ class Instance:
|
|||
def metrics_redis(self) -> Tuple[bool, dict[str, Any]]:
|
||||
return self.apiCaller.send_to_apis("GET", "/redis/stats", response=True)
|
||||
|
||||
def ping(self, plugin_id: Optional[str] = None) -> Tuple[bool, dict[str, Any]]:
|
||||
def ping(self, plugin_id: Optional[str] = None) -> Tuple[Union[bool, str], dict[str, Any]]:
|
||||
if not plugin_id:
|
||||
try:
|
||||
result = self.apiCaller.send_to_apis("GET", "/ping")[0]
|
||||
result = self.apiCaller.send_to_apis("GET", "/ping")
|
||||
except BaseException as e:
|
||||
return f"Can't ping {self.hostname}: {e}", {}
|
||||
return f"Can't ping instance {self.hostname}: {e}", {}
|
||||
|
||||
if result:
|
||||
return f"Instance {self.hostname} is up", {}
|
||||
return f"Can't ping {self.hostname}", {}
|
||||
if result[0]:
|
||||
return f"Instance {self.hostname} is up", result[1]
|
||||
return f"Can't ping instance {self.hostname}", result[1]
|
||||
return self.apiCaller.send_to_apis("POST", f"/{plugin_id}/ping", response=True)
|
||||
|
||||
def data(self, plugin_endpoint) -> Tuple[bool, dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Literal
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, verify_data_in_form
|
||||
from app.models.instance import Instance
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
||||
|
||||
instances = Blueprint("instances", __name__)
|
||||
|
||||
ACTIONS = {
|
||||
"reload": {"present": "Reloading", "past": "Reloaded"},
|
||||
"stop": {"present": "Stopping", "past": "Stopped"},
|
||||
"delete": {"present": "Deleting", "past": "Deleted"},
|
||||
}
|
||||
|
||||
|
||||
@instances.route("/instances", methods=["GET"])
|
||||
@login_required
|
||||
|
|
@ -55,40 +63,94 @@ def instances_new():
|
|||
return redirect(url_for("loading", next=url_for("instances.instances_page"), message=f"Creating new instance {instance['hostname']}"))
|
||||
|
||||
|
||||
@instances.route("/instances/<string:instance_hostname>/<string:action>", methods=["POST"])
|
||||
@instances.route("/instances/<string:action>", methods=["POST"])
|
||||
@login_required
|
||||
def instances_action(instance_hostname: str, action: Literal["ping", "reload", "stop", "delete"]): # TODO: see if we can support start and restart
|
||||
if action == "delete":
|
||||
delete_instance = None
|
||||
for instance in BW_INSTANCES_UTILS.get_instances():
|
||||
if instance.hostname == instance_hostname:
|
||||
delete_instance = instance
|
||||
break
|
||||
def instances_action(action: Literal["ping", "reload", "stop", "delete"]): # TODO: see if we can support start and restart
|
||||
verify_data_in_form(
|
||||
data={"instances": None},
|
||||
err_message=f"Missing instances parameter on /instances/{action}.",
|
||||
redirect_url="instances",
|
||||
next=True,
|
||||
)
|
||||
instances = request.form["instances"].split(",")
|
||||
if not instances:
|
||||
return handle_error("No instances selected.", "instances", True)
|
||||
|
||||
if not delete_instance:
|
||||
return handle_error(f"Instance {instance_hostname} not found.", "instances", True)
|
||||
if delete_instance.method != "ui":
|
||||
return handle_error(f"Instance {instance_hostname} is not a UI instance.", "instances", True)
|
||||
if action == "ping":
|
||||
succeed = []
|
||||
failed = []
|
||||
|
||||
ret = DB.delete_instance(instance_hostname)
|
||||
def ping_instance(instance):
|
||||
ret = Instance.from_hostname(instance, DB)
|
||||
if not ret:
|
||||
return {"hostname": instance, "message": f"The instance {instance} does not exist."}
|
||||
ret = ret.ping()
|
||||
if ret[0].startswith("Can't"):
|
||||
return {"hostname": instance, "message": ret[0]}
|
||||
return instance
|
||||
|
||||
with ThreadPoolExecutor() as executor:
|
||||
future_to_instance = {executor.submit(ping_instance, instance): instance for instance in instances}
|
||||
for future in as_completed(future_to_instance):
|
||||
instance = future.result()
|
||||
if isinstance(instance, dict):
|
||||
failed.append(instance)
|
||||
continue
|
||||
succeed.append(instance)
|
||||
|
||||
return jsonify({"succeed": succeed, "failed": failed}), 200
|
||||
elif action == "delete":
|
||||
delete_instances = set()
|
||||
non_ui_instances = set()
|
||||
for instance in DB.get_instances():
|
||||
if instance["hostname"] in instances:
|
||||
if instance["method"] != "ui":
|
||||
non_ui_instances.add(instance["hostname"])
|
||||
continue
|
||||
delete_instances.add(instance["hostname"])
|
||||
|
||||
for non_ui_instance in non_ui_instances:
|
||||
flash(f"Instance {non_ui_instance} is not a UI instance and will not be deleted.", "error")
|
||||
|
||||
if not delete_instances:
|
||||
return handle_error("All selected instances could not be found or are not UI instances.", "instances", True)
|
||||
|
||||
ret = DB.delete_instances(delete_instances)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't delete the instance in the database: {ret}", "instances", True)
|
||||
return handle_error(f"Couldn't delete the instances in the database: {ret}", "instances", True)
|
||||
flash(f"Instances {', '.join(delete_instances)} deleted successfully.", "success")
|
||||
else:
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(
|
||||
target=manage_bunkerweb,
|
||||
args=("instances", instance_hostname),
|
||||
kwargs={"operation": action, "threaded": True},
|
||||
).start()
|
||||
|
||||
def execute_action(instance):
|
||||
ret = Instance.from_hostname(instance, DB)
|
||||
if not ret:
|
||||
DATA["TO_FLASH"].append({"content": f"The instance {instance} does not exist.", "type": "error"})
|
||||
return
|
||||
|
||||
method = getattr(ret, action, None)
|
||||
if method is None or not callable(method):
|
||||
DATA["TO_FLASH"].append({"content": f"The instance {instance} does not have a {action} method.", "type": "error"})
|
||||
return
|
||||
|
||||
ret = method()
|
||||
if ret.startswith("Can't"):
|
||||
DATA["TO_FLASH"].append({"content": ret, "type": "error"})
|
||||
return
|
||||
DATA["TO_FLASH"].append({"content": f"Instance {instance} {ACTIONS[action]['past']} successfully.", "type": "success"})
|
||||
|
||||
def execute_actions(instances):
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.map(execute_action, instances)
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
Thread(target=execute_actions, args=(instances,)).start()
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"loading",
|
||||
next=url_for("instances.instances_page"),
|
||||
message=(
|
||||
(f"{action.title()}ing" if action not in ("delete", "stop") else ("Deleting" if action == "delete" else "Stopping"))
|
||||
+ f" instance {instance_hostname}"
|
||||
),
|
||||
message=(f"{ACTIONS[action]['present']} instances {', '.join(instances)}"),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22580,29 +22580,18 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes backgroundColorPhase {
|
||||
0% {
|
||||
background-color: var(--bs-primary); /* Start with primary color */
|
||||
}
|
||||
50% {
|
||||
background-color: var(--bs-bw-green); /* Transition to secondary color */
|
||||
}
|
||||
100% {
|
||||
background-color: var(--bs-primary); /* Back to primary color */
|
||||
}
|
||||
}
|
||||
|
||||
.buy-now .btn-buy-now {
|
||||
position: fixed;
|
||||
bottom: 3rem;
|
||||
right: 1.625rem;
|
||||
z-index: 1080;
|
||||
background-color: var(--bs-primary); /* Initial background color */
|
||||
box-shadow: 0 1px 20px 1px var(--bs-primary); /* Initial shadow */
|
||||
border-color: var(--bs-primary); /* Initial border color */
|
||||
background-color: var(--bs-primary); /* Initial background color */
|
||||
color: #fff;
|
||||
animation: colorPhase 3s infinite; /* Apply the color phasing animation */
|
||||
transition: box-shadow 0.3s ease-in-out; /* Smooth transition for box-shadow */
|
||||
transition:
|
||||
background-color 0.3s ease-in-out,
|
||||
box-shadow 0.3s ease-in-out; /* Smooth transitions */
|
||||
}
|
||||
|
||||
.buy-now .btn-buy-now:hover {
|
||||
|
|
@ -22613,26 +22602,6 @@ body {
|
|||
border-color: var(
|
||||
--bs-bw-green
|
||||
) !important; /* Keep the primary color on hover */
|
||||
animation: none; /* Pause the color phase animation on hover */
|
||||
animation: backgroundColorPhase 3s infinite; /* Apply the color phasing animation */
|
||||
}
|
||||
|
||||
.buy-now .btn-buy-now {
|
||||
position: fixed;
|
||||
bottom: 3rem;
|
||||
right: 1.625rem;
|
||||
z-index: 1080;
|
||||
box-shadow: 0 1px 20px 1px var(--bs-primary); /* Initial shadow */
|
||||
background-color: var(--bs-primary); /* Initial background color */
|
||||
color: #fff;
|
||||
transition:
|
||||
background-color 0.3s ease-in-out,
|
||||
box-shadow 0.3s ease-in-out; /* Smooth transitions */
|
||||
}
|
||||
|
||||
.buy-now .btn-buy-now:hover {
|
||||
box-shadow: none; /* Remove shadow on hover */
|
||||
animation: colorPhase 3s infinite; /* Start the color phase animation on hover */
|
||||
}
|
||||
|
||||
.ui-square,
|
||||
|
|
|
|||
|
|
@ -192,3 +192,77 @@
|
|||
.bg-bw-green {
|
||||
background-color: #2eac68;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.btn-responsive {
|
||||
padding: 4px 9px;
|
||||
font-size: 80%;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 992px) {
|
||||
.btn-responsive {
|
||||
padding: 8px 18px;
|
||||
font-size: 90%;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.pro-icon {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 15.5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pro-icon::before,
|
||||
.pro-icon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
transition: opacity 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
.pro-icon::before {
|
||||
background-image: url("../img/diamond.svg");
|
||||
opacity: 1;
|
||||
animation: fadeOut 1.5s infinite alternate;
|
||||
}
|
||||
|
||||
.pro-icon::after {
|
||||
background-image: url("../img/diamond-blue.svg");
|
||||
opacity: 0;
|
||||
animation: fadeIn 1.5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.min-vh-70 {
|
||||
min-height: 70vh !important;
|
||||
}
|
||||
|
||||
td.highlight {
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.1) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
td.highlight {
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.1) !important;
|
||||
:root {
|
||||
--dt-row-selected: 29, 123, 167;
|
||||
}
|
||||
|
||||
#loadingModal .modal-content {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
|
|
|||
43
src/ui/app/static/img/diamond-blue.svg
Normal file
43
src/ui/app/static/img/diamond-blue.svg
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Calque_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.56 39.31">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #5c8ea4;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #0b5577;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #aec6d2;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #083a4c;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #09435a;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #0a4c69;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Calque_2-2" data-name="Calque_2">
|
||||
<g id="Calque_1-2">
|
||||
<g>
|
||||
<polygon class="cls-1" points="40.37 5.19 31.4 10.38 14.11 10.38 5.19 5.19 13.91 0 31.65 0 40.37 5.19"/>
|
||||
<polygon class="cls-4" points="45.56 12.85 34.87 19.45 31.4 10.38 40.37 5.19 45.56 12.85"/>
|
||||
<polygon class="cls-5" points="45.56 12.85 22.73 39.31 34.87 19.45 45.56 12.85"/>
|
||||
<polygon class="cls-2" points="14.11 10.38 10.33 19.45 0 12.85 5.19 5.19 14.11 10.38"/>
|
||||
<polygon class="cls-6" points="22.73 39.31 0 12.85 10.33 19.45 22.73 39.31"/>
|
||||
<polygon class="cls-1" points="34.87 19.45 22.73 39.31 10.33 19.45 34.87 19.45"/>
|
||||
<polygon class="cls-3" points="34.87 19.45 10.33 19.45 14.11 10.38 31.4 10.38 34.87 19.45"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
43
src/ui/app/static/img/diamond.svg
Normal file
43
src/ui/app/static/img/diamond.svg
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Calque_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.56 39.31">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #237f4c;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #65b278;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #227848;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #349f53;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #194b34;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #249c59;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Calque_2-2" data-name="Calque_2">
|
||||
<g id="Calque_1-2">
|
||||
<g>
|
||||
<polygon class="cls-4" points="40.37 5.19 31.4 10.38 14.11 10.38 5.19 5.19 13.91 0 31.65 0 40.37 5.19"/>
|
||||
<polygon class="cls-3" points="45.56 12.85 34.87 19.45 31.4 10.38 40.37 5.19 45.56 12.85"/>
|
||||
<polygon class="cls-5" points="45.56 12.85 22.73 39.31 34.87 19.45 45.56 12.85"/>
|
||||
<polygon class="cls-6" points="14.11 10.38 10.33 19.45 0 12.85 5.19 5.19 14.11 10.38"/>
|
||||
<polygon class="cls-1" points="22.73 39.31 0 12.85 10.33 19.45 22.73 39.31"/>
|
||||
<polygon class="cls-4" points="34.87 19.45 22.73 39.31 10.33 19.45 34.87 19.45"/>
|
||||
<polygon class="cls-2" points="34.87 19.45 10.33 19.45 14.11 10.38 31.4 10.38 34.87 19.45"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,19 +1,205 @@
|
|||
$(document).ready(function () {
|
||||
const getSelectedInstances = (action) => {
|
||||
const instances = [];
|
||||
$("tr.selected").each(function () {
|
||||
instances.push($(this).find("td:first").text());
|
||||
});
|
||||
return instances;
|
||||
};
|
||||
var toastNum = 0;
|
||||
var actionLock = false;
|
||||
|
||||
$.fn.dataTable.ext.buttons.create_instance = {
|
||||
text: "Create new instance",
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
action: function (e, dt, node, config) {
|
||||
var modal = new bootstrap.Modal($("#modal-create-instance"));
|
||||
const modal = new bootstrap.Modal($("#modal-create-instance"));
|
||||
modal.show();
|
||||
|
||||
$("#modal-create-instance").on("shown.bs.modal", function () {
|
||||
$(this).find("#hostname").focus();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.ping_instances = {
|
||||
text: '<span class="tf-icons bx bx-bell bx-18px me-md-2"></span><span class="d-none d-md-inline">Ping</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
|
||||
const instances = getSelectedInstances("ping");
|
||||
if (instances.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (actionLock) {
|
||||
$("#loadingModal").modal("show");
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Create a FormData object
|
||||
const formData = new FormData();
|
||||
formData.append("csrf_token", $("#csrf_token").val()); // Add the CSRF token
|
||||
formData.append("instances", instances.join(",")); // Add the instances
|
||||
|
||||
// Send the form data using $.ajax
|
||||
$.ajax({
|
||||
url: `${window.location.pathname}/ping`,
|
||||
type: "POST",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (data) {
|
||||
data.failed.forEach((instance) => {
|
||||
var feedbackToastFailed = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToastFailed.attr("id", `feedback-toast-${toastNum++}`); // Corrected to set the ID for the failed toast
|
||||
feedbackToastFailed.removeClass("bg-primary text-white");
|
||||
feedbackToastFailed.addClass("bg-danger text-white");
|
||||
feedbackToastFailed.find("span").text("Ping failed");
|
||||
feedbackToastFailed.find("div.toast-body").text(instance.message);
|
||||
feedbackToastFailed.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
feedbackToastFailed.toast("show");
|
||||
});
|
||||
|
||||
if (data.succeed.length > 0) {
|
||||
var feedbackToastSucceed = $("#feedback-toast").clone(); // Clone the feedback toast
|
||||
feedbackToastSucceed.attr("id", `feedback-toast-${toastNum++}`);
|
||||
feedbackToastSucceed.addClass("bg-primary text-white");
|
||||
feedbackToastSucceed.find("span").text("Ping successful");
|
||||
feedbackToastSucceed
|
||||
.find("div.toast-body")
|
||||
.text(`Instances: ${data.succeed.join(", ")}`);
|
||||
feedbackToastSucceed.appendTo("#feedback-toast-container");
|
||||
feedbackToastSucceed.toast("show");
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error("AJAX request failed:", status, error);
|
||||
alert("An error occurred while pinging the instances.");
|
||||
},
|
||||
complete: function () {
|
||||
actionLock = false;
|
||||
$("#loadingModal").modal("hide");
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.exec_form = {
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
|
||||
const instances = getSelectedInstances("ping");
|
||||
if (instances.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const bxIcon = node.find("span.bx").attr("class").split(" ")[2];
|
||||
if (bxIcon === "bx-refresh") {
|
||||
var action = "reload";
|
||||
} else if (bxIcon === "bx-stop") {
|
||||
var action = "stop";
|
||||
} else {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a form element using jQuery and set its attributes
|
||||
const $form = $("<form>", {
|
||||
method: "POST",
|
||||
action: `${window.location.pathname}/${action}`,
|
||||
class: "visually-hidden",
|
||||
});
|
||||
|
||||
// Add CSRF token and instances as hidden inputs
|
||||
$form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
}),
|
||||
);
|
||||
$form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "instances",
|
||||
value: instances.join(","),
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
$form.appendTo("body").submit();
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete_instances = {
|
||||
text: '<span class="tf-icons bx bx-trash bx-18px me-md-2"></span><span class="d-none d-md-inline">Delete</span>',
|
||||
className: "btn btn-sm btn-outline-danger",
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
|
||||
const instances = getSelectedInstances("ping");
|
||||
if (instances.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$("#selected-instances-input").val(instances.join(","));
|
||||
|
||||
const delete_modal = $("#modal-delete-instances");
|
||||
instances.forEach((instance) => {
|
||||
// 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">${instance}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
|
||||
// Clone the status element and append it to the list item
|
||||
const statusClone = $("#status-" + instance).clone();
|
||||
listItem.append(statusClone);
|
||||
|
||||
// Append the list item to the list
|
||||
$("#selected-instances").append(listItem);
|
||||
});
|
||||
|
||||
const modal = new bootstrap.Modal(delete_modal);
|
||||
modal.show();
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
};
|
||||
|
||||
const instances_table = new DataTable("#instances", {
|
||||
columnDefs: [{ orderable: false, targets: 7 }],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("text-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[6, "desc"]],
|
||||
autoFill: false,
|
||||
colReorder: true,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
},
|
||||
layout: {
|
||||
topStart: {
|
||||
pageLength: {
|
||||
|
|
@ -22,7 +208,7 @@ $(document).ready(function () {
|
|||
buttons: [
|
||||
{
|
||||
extend: "colvis",
|
||||
columns: "th:not(:first-child):not(:last-child)",
|
||||
columns: "th:not(:first-child)",
|
||||
text: "Columns",
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
columnText: function (dt, idx, title) {
|
||||
|
|
@ -74,6 +260,27 @@ $(document).ready(function () {
|
|||
},
|
||||
],
|
||||
},
|
||||
bottomEnd: {
|
||||
buttons: [
|
||||
{
|
||||
extend: "ping_instances",
|
||||
},
|
||||
{
|
||||
extend: "exec_form",
|
||||
text: '<span class="tf-icons bx bx-refresh bx-18px me-md-2"></span><span class="d-none d-md-inline">Reload</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
},
|
||||
{
|
||||
extend: "exec_form",
|
||||
text: '<span class="tf-icons bx bx-stop bx-18px me-md-2"></span><span class="d-none d-md-inline">Stop</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
},
|
||||
{
|
||||
extend: "delete_instances",
|
||||
},
|
||||
],
|
||||
paging: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -91,24 +298,16 @@ $(document).ready(function () {
|
|||
.each((el) => el.classList.add("highlight"));
|
||||
});
|
||||
|
||||
$(document).on("click", "button[data-action]", function () {
|
||||
const form = $(this).closest("form");
|
||||
const action = $(this).data("action"); // Get the action from the button
|
||||
const actionSplit = form.attr("action").split("/");
|
||||
const instanceHostname = actionSplit[actionSplit.length - 1];
|
||||
|
||||
if (
|
||||
action === "delete" &&
|
||||
$(`#method-${instanceHostname}`).text() !== "ui"
|
||||
) {
|
||||
return;
|
||||
} else if ($(`#status-${instanceHostname}`).text() !== "Up") {
|
||||
return;
|
||||
$(document).on("hidden.bs.toast", ".toast", function (event) {
|
||||
if (event.target.id.startsWith("feedback-toast")) {
|
||||
setTimeout(() => {
|
||||
$(this).remove();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
form.attr("action", `${form.attr("action")}/${action}`);
|
||||
|
||||
// Now, submit the form with the updated action
|
||||
form.off("submit").submit();
|
||||
$("#modal-delete-instances").on("hidden.bs.modal", function () {
|
||||
$("#selected-instances").empty();
|
||||
$("#selected-instances-input").val("");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,33 +44,14 @@
|
|||
{% include "sidebar.html" %}
|
||||
{% if not is_pro_version %}
|
||||
<div class="buy-now">
|
||||
<a class="btn btn-success btn-buy-now"
|
||||
<a class="btn btn-responsive btn-buy-now"
|
||||
role="button"
|
||||
aria-pressed="true"
|
||||
href="https://panel.bunkerweb.io/order/bunkerweb-pro/?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<span class="tf-icons me-2 d-flex h-100 justify-content-center align-items-center">
|
||||
<svg width="19"
|
||||
height="15"
|
||||
viewBox="0 0 19 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_226_466)">
|
||||
<path d="M16.8356 1.98041L13.0948 3.96082H5.88436L2.16443 1.98041L5.80095 0H13.1991L16.8356 1.98041Z" fill="#349F53" />
|
||||
<path d="M19 4.90339L14.542 7.42183L13.0948 3.96088L16.8356 1.98047L19 4.90339Z" fill="#227848" />
|
||||
<path d="M19 4.90332L9.47913 15L14.5419 7.42176L19 4.90332Z" fill="#194B34" />
|
||||
<path d="M5.88433 3.96088L4.30795 7.42183L0 4.90339L2.1644 1.98047L5.88433 3.96088Z" fill="#249C59" />
|
||||
<path d="M9.47915 15L0 4.90332L4.30795 7.42176L9.47915 15Z" fill="#237F4C" />
|
||||
<path d="M14.542 7.42175L9.47919 15L4.30798 7.42175H14.542Z" fill="#349F53" />
|
||||
<path d="M14.542 7.42177H4.30798L5.88437 3.96082H13.0949L14.542 7.42177Z" fill="#65B278" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_226_466">
|
||||
<rect width="19" height="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="pro-icon"></div>
|
||||
</span>
|
||||
Upgrade to Pro</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="toast-container position-fixed bottom-0 end-0 mb-3 me-3">
|
||||
<div id="feedback-toast-container" class="toast-container position-fixed bottom-0 end-0 mb-3 me-3">
|
||||
<!-- prettier-ignore -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if pro_overlapped %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 h-70">
|
||||
<div class="modal fade"
|
||||
id="loadingModal"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-transparent border-0 shadow-none">
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-white">Pinging instances, please wait...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="instances" class="table w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -12,7 +32,6 @@
|
|||
<th>Type</th>
|
||||
<th>Creation date</th>
|
||||
<th>Last seen</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -36,62 +55,28 @@
|
|||
<td>{{ instance.type }}</td>
|
||||
<td>{{ instance.creation_date.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>{{ instance.last_seen.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>
|
||||
<form id="instance-form"
|
||||
action="{{ url_for("instances") }}/{{ instance.hostname }}"
|
||||
method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if instance.status != "up" %}
|
||||
{% set disabled = "disabled" %}
|
||||
{% else %}
|
||||
{% set disabled = "" %}
|
||||
{% endif %}
|
||||
{% if instance.method == "ui" %}
|
||||
{% set can_delete = "" %}
|
||||
{% else %}
|
||||
{% set can_delete = "disabled" %}
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group" aria-label="Action group">
|
||||
<button type="button"
|
||||
data-action="ping"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-original-title="Ping"
|
||||
class="btn btn-sm btn-outline-primary {{ disabled }}">
|
||||
<i class="tf-icons bx bx-xs bx-bell"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="stop"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-original-title="Stop"
|
||||
class="btn btn-sm btn-outline-primary {{ disabled }}">
|
||||
<i class="tf-icons bx bx-xs bx-stop"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="reload"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-original-title="Reload"
|
||||
class="btn btn-sm btn-outline-primary {{ disabled }}">
|
||||
<i class="tf-icons bx bx-xs bx-refresh"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="delete"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-original-title="Delete"
|
||||
class="btn btn-sm btn-outline-danger {{ can_delete }}">
|
||||
<i class="tf-icons bx bx-xs bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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, ck-t means that you're curious 👀</div>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="modal-create-instance"
|
||||
data-bs-backdrop="static"
|
||||
|
|
@ -130,10 +115,46 @@
|
|||
maxlength="256"
|
||||
required />
|
||||
</div>
|
||||
<div class="alert alert-primary text-center" role="alert">You don't need to provide the port or the server_name as the values of both <code>API_HTTP_PORT</code> and <code>API_SERVER_NAME</code> will be used for the instance configuration.</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-primary me-2">Create Instance</button>
|
||||
<button type="reset" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="modal-delete-instances"
|
||||
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 instances</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for("instances") }}/delete" method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" id="selected-instances-input" name="instances" value="" />
|
||||
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected instances?</div>
|
||||
<ul id="selected-instances" class="list-group mb-3">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Delete instances</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,55 +4,8 @@
|
|||
<a href="https://www.bunkerweb.io/?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
class="app-brand-link">
|
||||
<span class="app-brand-logo main">
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 272.68 76.37"
|
||||
xml:space="preserve">
|
||||
<style type="text/css" nonce="{{ style_nonce }}">
|
||||
.st0 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st1 {
|
||||
fill: #2eac68;
|
||||
}
|
||||
.st2 {
|
||||
fill: #0b5577;
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st2" d="M244.87,44.46c0.16-1.27,1.3-2.12,2.59-1.95c1.01,0.13,1.85,0.96,1.92,2.05c0.14,1.52,0.26,2.71,0.36,3.85 h16.02c-0.24-1.79-0.49-3.61-0.82-6c-0.1-0.8,0.25-1.61,0.9-2.02c3.4-2.43,5.46-6.75,6.49-12.5c2.34-13.05-7.24-18.86-18.92-20.2 c-7.49-1-11.24-1.45-18.75-2.29c-1.35-0.15-2.51,0.72-2.65,2c-1.48,13.67-2.97,27.34-4.45,41.01h16.79 C244.51,47.22,244.66,46.05,244.87,44.46z M243.31,21.88c2.52,0.56,3.78,0.85,6.29,1.43c0.87-0.8,2.08-1.22,3.37-1.03 c2.21,0.32,3.75,2.31,3.43,4.42c-0.32,2.11-2.36,3.55-4.55,3.23c-1.27-0.19-2.31-0.93-2.91-1.94c-2.56-0.14-3.84-0.2-6.41-0.32 C242.84,25.36,242.99,24.2,243.31,21.88z" />
|
||||
<g>
|
||||
<path class="st2" d="M224.95,41c0.11-1.06-0.73-2.08-1.81-2.19c-6.15-0.63-9.23-0.91-15.38-1.43c-1.16-0.1-2.02-1.09-1.93-2.23 c0.01-0.08,0.01-0.13,0.02-0.21c0.09-1.06,1.11-1.91,2.27-1.81c4.44,0.37,6.67,0.57,11.11,1c1.09,0.11,2.12-0.72,2.24-1.85 c0.35-3.56,0.53-5.34,0.88-8.91c0.1-1.06-0.76-2.07-1.87-2.18c-4.51-0.44-6.76-0.64-11.28-1.02c-1.18-0.1-2.06-1.1-1.97-2.23 c0.09-1.06,1.12-1.9,2.31-1.8c6.31,0.53,9.47,0.82,15.77,1.46c1.11,0.11,2.17-0.7,2.28-1.84c0.38-3.62,0.57-5.43,0.94-9.04 c0.11-1.06-0.77-2.08-1.9-2.19c-11.16-1.06-22.29-1.98-33.47-2.77c-1.2-0.08-2.24,0.79-2.3,1.85 c-0.89,14.49-1.77,28.97-2.66,43.46c-0.03,0.5,0.13,0.96,0.4,1.33h35.57C224.45,45.8,224.63,44.03,224.95,41z" />
|
||||
<g>
|
||||
<path class="st2" d="M185.93,46.8c-2.49-8.91-3.77-13.36-6.42-22.25c-0.06-0.29-0.04-0.71,0.05-0.99 c3.19-8.01,4.82-12,8.14-19.98c0.44-1.04-0.33-2.15-1.53-2.22c-5.45-0.3-8.18-0.43-13.64-0.65c-0.75-0.03-1.45,0.44-1.7,1.07 c-2.18,5.75-3.25,8.62-5.37,14.38c-0.24,0.63-0.92,1.11-1.66,1.08c-0.96-0.03-1.68-0.76-1.65-1.68 c0.16-5.42,0.23-8.14,0.39-13.56c0.03-0.92-0.71-1.65-1.69-1.68c-5.61-0.15-8.41-0.2-14.02-0.26c-0.98-0.01-1.74,0.69-1.75,1.62 c-0.13,14.77-0.26,29.55-0.39,44.32c-0.01,0.92,0.7,1.64,1.63,1.65c5.31,0.06,7.96,0.11,13.26,0.25 c0.93,0.02,1.66-0.66,1.69-1.59c0.14-5.03,0.22-7.54,0.36-12.57c0.03-0.92,0.77-1.61,1.72-1.58c0.06,0,0.09,0,0.15,0 c0.73,0.02,1.29,0.47,1.56,1.12c1.82,5.55,2.72,8.32,4.48,13.88c0.26,0.65,0.81,1.1,1.53,1.13c1.23,0.05,2.31,0.1,3.32,0.14 h11.12C185.9,48,186.1,47.41,185.93,46.8z" />
|
||||
<g>
|
||||
<path class="st2" d="M109.16,48.06c1.64-0.06,2.95-1.44,2.9-3.08c-0.14-4.49-0.2-6.73-0.34-11.22 c-0.03-0.92,0.68-1.51,1.48-1.54c0.51-0.01,0.95,0.19,1.25,0.68c3.54,5.28,5.29,7.92,8.72,13.24c0.52,0.99,1.52,1.54,2.67,1.52 c4.62-0.06,6.93-0.08,11.55-0.08c1.64,0,3-1.34,3.01-2.98c0.05-13.87,0.1-27.75,0.16-41.62c0.01-1.63-1.42-2.99-3.16-2.99 c-4.46,0-6.7,0.01-11.16,0.07c-1.73,0.02-3.14,1.39-3.12,3.03c0.07,4.09,0.1,6.14,0.17,10.23c0.01,0.85-0.72,1.43-1.53,1.45 c-0.45,0.01-0.97-0.2-1.27-0.62c-3.6-4.96-5.42-7.43-9.12-12.35c-0.55-0.84-1.62-1.37-2.76-1.34 c-4.88,0.16-7.33,0.26-12.21,0.48c-1.73,0.08-3.1,1.5-3.01,3.13c0.69,13.86,1.39,27.71,2.08,41.57 c0.07,1.37,1.09,2.48,2.38,2.76h2.4C103.45,48.26,105.53,48.18,109.16,48.06z" />
|
||||
<g>
|
||||
<path class="st2" d="M90.68,31.54c-0.59-10.75-0.89-16.13-1.48-26.88c-0.1-1.84-1.76-3.25-3.72-3.13 C81.33,1.77,79.25,1.91,75.1,2.2c-1.96,0.14-3.43,1.74-3.29,3.59c0.83,11.11,1.25,16.66,2.08,27.76 c0.23,3.05-1.38,3.88-4.2,4.1c-2.82,0.23-4.61-0.33-4.87-3.37c-0.95-11.1-1.43-16.64-2.38-27.74 c-0.16-1.84-1.86-3.19-3.82-3.02c-4.12,0.37-6.18,0.57-10.29,0.98c-2.03,0.21-3.45,1.86-3.25,3.69 c1.14,10.82,1.72,16.23,2.86,27.05c0.61,6.46,3.39,10.67,7.45,13.16H81.6C87.6,45.46,91.41,40.09,90.68,31.54z" />
|
||||
<g>
|
||||
<path class="st2" d="M45.57,39.08c-0.36-3.32-1.02-6.96-4.2-8.75c-1.34-0.76-1.85-2.58-0.95-3.82 c1.9-2.86,1.84-6.21,1.48-9.46c-0.87-7.84-7.46-11.1-19.67-9.49c-7.79,1.03-11.68,1.58-19.45,2.79 c-1.79,0.28-3,1.84-2.74,3.52c1.82,11.52,3.65,23.04,5.47,34.55h37.72C45.2,45.98,45.98,42.89,45.57,39.08z M19.57,41.45 c-0.16-3.39-0.24-5.08-0.39-8.47c-1.42-0.79-2.47-2.16-2.7-3.84c-0.41-2.91,1.72-5.62,4.76-6.03 c3.04-0.41,5.81,1.63,6.19,4.55c0.22,1.69-0.43,3.28-1.59,4.42c0.76,3.31,1.13,4.96,1.88,8.27 C24.46,40.77,22.83,40.99,19.57,41.45z" />
|
||||
</g>
|
||||
</g>
|
||||
<path class="st2" d="M100.25,48.41h-2.4c0.25,0.05,0.5,0.09,0.76,0.08C99.2,48.46,99.73,48.43,100.25,48.41z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="89.49" y="51.91" class="st1" width="93.69" height="24.46" />
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M121.14,73.93l-1.91,0.03l-3.43-9.9l-3.34,9.93l-1.91-0.01l-3.25-14.33l-0.75,0c-0.37,0-0.63-0.07-0.79-0.22 s-0.24-0.34-0.24-0.57c0-0.22,0.08-0.4,0.24-0.55c0.16-0.15,0.42-0.22,0.79-0.22l4.24-0.02c0.37,0,0.63,0.07,0.79,0.22 c0.16,0.15,0.24,0.34,0.24,0.57c0,0.22-0.08,0.4-0.24,0.55s-0.42,0.22-0.79,0.22l-1.96,0.01l2.78,12.22l3.25-9.78l1.84-0.01 l3.42,9.76l2.58-12.24l-1.95,0.01c-0.37,0-0.63-0.07-0.8-0.22c-0.16-0.15-0.25-0.34-0.25-0.57c0-0.22,0.08-0.4,0.24-0.55 c0.16-0.15,0.43-0.22,0.81-0.22l4.22-0.02c0.38,0,0.65,0.07,0.81,0.22c0.16,0.15,0.24,0.34,0.24,0.57c0,0.22-0.08,0.4-0.24,0.55 s-0.43,0.22-0.81,0.22l-0.73,0L121.14,73.93z" />
|
||||
<path class="st0" d="M146.71,66.25l-15.72,0.06c0.28,1.99,1.12,3.6,2.52,4.81c1.41,1.21,3.14,1.82,5.21,1.81 c1.15,0,2.36-0.2,3.62-0.58c1.26-0.38,2.29-0.89,3.08-1.52c0.23-0.18,0.43-0.28,0.6-0.28c0.2,0,0.37,0.08,0.51,0.23 c0.15,0.15,0.22,0.33,0.22,0.54s-0.1,0.41-0.29,0.61c-0.59,0.61-1.63,1.19-3.12,1.73c-1.5,0.54-3.04,0.81-4.62,0.82 c-2.64,0.01-4.85-0.85-6.63-2.57c-1.78-1.72-2.67-3.82-2.68-6.28c-0.01-2.24,0.81-4.17,2.47-5.78s3.7-2.42,6.15-2.43 c2.52-0.01,4.6,0.81,6.23,2.45C145.91,61.5,146.72,63.63,146.71,66.25z M145.14,64.7c-0.31-1.7-1.12-3.08-2.43-4.14 c-1.31-1.06-2.86-1.59-4.66-1.58c-1.8,0.01-3.35,0.54-4.64,1.6s-2.1,2.45-2.41,4.18L145.14,64.7z" />
|
||||
<path class="st0" d="M155.06,50.76l0.04,10.23c1.85-2.43,4.09-3.65,6.73-3.66c2.25-0.01,4.18,0.8,5.79,2.43 c1.61,1.63,2.42,3.63,2.43,6.01c0.01,2.4-0.8,4.43-2.41,6.11c-1.62,1.67-3.53,2.51-5.75,2.52c-2.69,0.01-4.94-1.19-6.75-3.61 l0.01,3.03l-3.62,0.01c-0.37,0-0.63-0.07-0.79-0.22c-0.16-0.15-0.24-0.33-0.24-0.55c0-0.23,0.08-0.42,0.24-0.56 c0.16-0.14,0.42-0.21,0.79-0.21l2.07-0.01l-0.07-19.94l-2.07,0.01c-0.37,0-0.63-0.07-0.79-0.22c-0.16-0.15-0.24-0.34-0.24-0.57 c0-0.22,0.08-0.4,0.24-0.55c0.16-0.15,0.42-0.22,0.79-0.22L155.06,50.76z M168.5,65.84c-0.01-1.95-0.68-3.59-2.02-4.94 c-1.34-1.35-2.9-2.02-4.69-2.01s-3.35,0.69-4.67,2.05c-1.33,1.36-1.99,3.01-1.98,4.96c0.01,1.95,0.68,3.59,2.02,4.94 c1.34,1.35,2.9,2.02,4.69,2.01c1.79-0.01,3.35-0.69,4.67-2.05C167.85,69.44,168.51,67.79,168.5,65.84z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="app-brand-logo main w-100">
|
||||
<img class="img-fluid" src="img/logo-menu.png" alt="BunkerWeb logo">
|
||||
</span>
|
||||
</a>
|
||||
<a href="javascript:void(0);"
|
||||
|
|
@ -65,7 +18,7 @@
|
|||
"home": {"url": url_for('home'), "icon": "bx-home-smile"},
|
||||
"instances": {"url": url_for('instances'), "icon": "bx-server"},
|
||||
"global-config": {"url": url_for('global_config'), "icon": "bx-cog"},
|
||||
"services": {"url": url_for('services'), "icon": "bx-cube", "sub": services, "max": 5},
|
||||
"services": {"url": url_for('services'), "icon": "bx-cube"},
|
||||
"configs": {"url": url_for('configs'), "icon": "bx-wrench"},
|
||||
"plugins": {"url": url_for('plugins'), "icon": "bx-plug"},
|
||||
"cache": {"url": url_for('cache'), "icon": "bx-data"},
|
||||
|
|
@ -123,26 +76,7 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="badge badge-center rounded-pill text-uppercase fs-tiny ms-auto">
|
||||
<svg width="19"
|
||||
height="15"
|
||||
viewBox="0 0 19 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_226_466)">
|
||||
<path d="M16.8356 1.98041L13.0948 3.96082H5.88436L2.16443 1.98041L5.80095 0H13.1991L16.8356 1.98041Z" fill="#349F53" />
|
||||
<path d="M19 4.90339L14.542 7.42183L13.0948 3.96088L16.8356 1.98047L19 4.90339Z" fill="#227848" />
|
||||
<path d="M19 4.90332L9.47913 15L14.5419 7.42176L19 4.90332Z" fill="#194B34" />
|
||||
<path d="M5.88433 3.96088L4.30795 7.42183L0 4.90339L2.1644 1.98047L5.88433 3.96088Z" fill="#249C59" />
|
||||
<path d="M9.47915 15L0 4.90332L4.30795 7.42176L9.47915 15Z" fill="#237F4C" />
|
||||
<path d="M14.542 7.42175L9.47919 15L4.30798 7.42175H14.542Z" fill="#349F53" />
|
||||
<path d="M14.542 7.42177H4.30798L5.88437 3.96082H13.0949L14.542 7.42177Z" fill="#65B278" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_226_466">
|
||||
<rect width="19" height="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<img src="img/diamond.svg" alt="Pro plugin" width="18px" height="15.5px">
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
|
|
|||
Loading…
Reference in a new issue