mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add reports, bans, logs and jobs page to web UI
This commit is contained in:
parent
b75a0fe5f5
commit
c6d9846279
37 changed files with 3021 additions and 342 deletions
|
|
@ -2057,7 +2057,7 @@ class Database:
|
|||
|
||||
return custom_config
|
||||
|
||||
def upsert_custom_config(self, config_type: str, name: str, config: Dict[str, Any], *, service_id: Optional[str] = None) -> str:
|
||||
def upsert_custom_config(self, config_type: str, name: str, config: Dict[str, Any], *, service_id: Optional[str] = None, new: bool = False) -> str:
|
||||
"""Update or insert a custom config in the database"""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
|
|
@ -2087,6 +2087,8 @@ class Database:
|
|||
)
|
||||
)
|
||||
else:
|
||||
if new:
|
||||
return "The custom config already exists"
|
||||
custom_config.service_id = config.get("service_id")
|
||||
custom_config.data = data
|
||||
custom_config.checksum = checksum
|
||||
|
|
@ -3181,8 +3183,8 @@ class Database:
|
|||
"reload": job.reload,
|
||||
"history": [
|
||||
{
|
||||
"start_date": job_run.start_date.strftime("%Y/%m/%d, %H:%M:%S %Z"),
|
||||
"end_date": job_run.end_date.strftime("%Y/%m/%d, %H:%M:%S %Z"),
|
||||
"start_date": job_run.start_date.isoformat(),
|
||||
"end_date": job_run.end_date.isoformat(),
|
||||
"success": job_run.success,
|
||||
}
|
||||
for job_run in session.query(Jobs_runs)
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ class InstancesUtils:
|
|||
def get_instance_bans(instance: Instance) -> List[dict[str, Any]]:
|
||||
resp, instance_bans = instance.bans()
|
||||
if resp:
|
||||
self.__db.logger.warning(resp)
|
||||
return []
|
||||
return instance_bans[instance.hostname].get("data", [])
|
||||
|
||||
|
|
@ -226,8 +225,7 @@ class InstancesUtils:
|
|||
|
||||
def get_instance_reports(instance: Instance) -> Tuple[bool, dict[str, Any]]:
|
||||
resp, instance_reports = instance.reports()
|
||||
if resp:
|
||||
self.__db.logger.warning(resp)
|
||||
if not resp:
|
||||
return []
|
||||
return (instance_reports[instance.hostname].get("msg") or {"requests": []}).get("requests", [])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,123 +1,26 @@
|
|||
from datetime import datetime
|
||||
from json import dumps, loads as json_loads
|
||||
from json import JSONDecodeError, dumps, loads
|
||||
from math import floor
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
from redis import Redis, Sentinel
|
||||
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
|
||||
from app.utils import LOGGER
|
||||
from app.dependencies import BW_INSTANCES_UTILS
|
||||
|
||||
from app.routes.utils import get_remain, handle_error, verify_data_in_form
|
||||
from app.routes.utils import get_redis_client, get_remain, handle_error, verify_data_in_form
|
||||
|
||||
bans = Blueprint("bans", __name__)
|
||||
|
||||
|
||||
@bans.route("/bans", methods=["GET", "POST"])
|
||||
@bans.route("/bans", methods=["GET"])
|
||||
@login_required
|
||||
def bans_page():
|
||||
if request.method == "POST":
|
||||
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "bans")
|
||||
|
||||
# Check variables
|
||||
verify_data_in_form(data={"operation": ("ban", "unban")}, err_message="Invalid operation parameter on /bans.", redirect_url="bans")
|
||||
verify_data_in_form(data={"data": None}, err_message="Missing data parameter on /bans.", redirect_url="bans")
|
||||
|
||||
redis_client = None
|
||||
db_config = BW_CONFIG.get_config(
|
||||
global_only=True,
|
||||
methods=False,
|
||||
filtered_settings=(
|
||||
"USE_REDIS",
|
||||
"REDIS_HOST",
|
||||
"REDIS_PORT",
|
||||
"REDIS_DB",
|
||||
"REDIS_TIMEOUT",
|
||||
"REDIS_KEEPALIVE_POOL",
|
||||
"REDIS_SSL",
|
||||
"REDIS_USERNAME",
|
||||
"REDIS_PASSWORD",
|
||||
"REDIS_SENTINEL_HOSTS",
|
||||
"REDIS_SENTINEL_USERNAME",
|
||||
"REDIS_SENTINEL_PASSWORD",
|
||||
"REDIS_SENTINEL_MASTER",
|
||||
),
|
||||
)
|
||||
use_redis = db_config.get("USE_REDIS", "no") == "yes"
|
||||
redis_host = db_config.get("REDIS_HOST")
|
||||
if use_redis and redis_host:
|
||||
redis_port = db_config.get("REDIS_PORT", "6379")
|
||||
if not redis_port.isdigit():
|
||||
redis_port = "6379"
|
||||
redis_port = int(redis_port)
|
||||
|
||||
redis_db = db_config.get("REDIS_DB", "0")
|
||||
if not redis_db.isdigit():
|
||||
redis_db = "0"
|
||||
redis_db = int(redis_db)
|
||||
|
||||
redis_timeout = db_config.get("REDIS_TIMEOUT", "1000.0")
|
||||
try:
|
||||
redis_timeout = float(redis_timeout)
|
||||
except ValueError:
|
||||
redis_timeout = 1000.0
|
||||
|
||||
redis_keepalive_pool = db_config.get("REDIS_KEEPALIVE_POOL", "10")
|
||||
if not redis_keepalive_pool.isdigit():
|
||||
redis_keepalive_pool = "10"
|
||||
redis_keepalive_pool = int(redis_keepalive_pool)
|
||||
|
||||
redis_ssl = db_config.get("REDIS_SSL", "no") == "yes"
|
||||
username = db_config.get("REDIS_USERNAME", None) or None
|
||||
password = db_config.get("REDIS_PASSWORD", None) or None
|
||||
sentinel_hosts = db_config.get("REDIS_SENTINEL_HOSTS", [])
|
||||
|
||||
if isinstance(sentinel_hosts, str):
|
||||
sentinel_hosts = [host.split(":") if ":" in host else (host, "26379") for host in sentinel_hosts.split(" ") if host]
|
||||
|
||||
if sentinel_hosts:
|
||||
sentinel_username = db_config.get("REDIS_SENTINEL_USERNAME", None) or None
|
||||
sentinel_password = db_config.get("REDIS_SENTINEL_PASSWORD", None) or None
|
||||
sentinel_master = db_config.get("REDIS_SENTINEL_MASTER", "")
|
||||
|
||||
sentinel = Sentinel(
|
||||
sentinel_hosts,
|
||||
username=sentinel_username,
|
||||
password=sentinel_password,
|
||||
ssl=redis_ssl,
|
||||
socket_timeout=redis_timeout,
|
||||
socket_connect_timeout=redis_timeout,
|
||||
socket_keepalive=True,
|
||||
max_connections=redis_keepalive_pool,
|
||||
)
|
||||
redis_client = sentinel.slave_for(sentinel_master, db=redis_db, username=username, password=password)
|
||||
else:
|
||||
redis_client = Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
db=redis_db,
|
||||
username=username,
|
||||
password=password,
|
||||
socket_timeout=redis_timeout,
|
||||
socket_connect_timeout=redis_timeout,
|
||||
socket_keepalive=True,
|
||||
max_connections=redis_keepalive_pool,
|
||||
ssl=redis_ssl,
|
||||
)
|
||||
|
||||
try:
|
||||
redis_client.ping()
|
||||
except BaseException:
|
||||
redis_client = None
|
||||
flash("Couldn't connect to redis, ban list might be incomplete", "error")
|
||||
redis_client = get_redis_client()
|
||||
|
||||
def get_load_data():
|
||||
try:
|
||||
data = json_loads(request.form["data"])
|
||||
data = loads(request.form["data"])
|
||||
assert isinstance(data, list)
|
||||
return data
|
||||
except BaseException:
|
||||
|
|
@ -128,10 +31,9 @@ def bans_page():
|
|||
|
||||
for unban in data:
|
||||
try:
|
||||
unban = json_loads(unban.replace('"', '"').replace("'", '"'))
|
||||
unban = loads(unban.replace('"', '"').replace("'", '"'))
|
||||
except BaseException:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
LOGGER.exception(f"Couldn't unban {unban['ip']}")
|
||||
continue
|
||||
|
||||
if "ip" not in unban:
|
||||
|
|
@ -190,7 +92,7 @@ def bans_page():
|
|||
if not data:
|
||||
continue
|
||||
exp = redis_client.ttl(key)
|
||||
bans.append({"ip": ip, "exp": exp} | json_loads(data)) # type: ignore
|
||||
bans.append({"ip": ip, "exp": exp} | loads(data)) # type: ignore
|
||||
instance_bans = BW_INSTANCES_UTILS.get_bans()
|
||||
|
||||
# Prepare data
|
||||
|
|
@ -200,22 +102,96 @@ def bans_page():
|
|||
if not any(b["ip"] == ban["ip"] for b in bans):
|
||||
bans.append(ban)
|
||||
|
||||
# Get the last 100 bans
|
||||
bans = bans[:100]
|
||||
reasons = set()
|
||||
remains = set()
|
||||
|
||||
for ban in bans:
|
||||
exp = ban.pop("exp", 0)
|
||||
# Add remain
|
||||
remain = ("unknown", "unknown") if exp <= 0 else get_remain(exp)
|
||||
ban["remain"] = remain[0]
|
||||
remains.add(remain[1])
|
||||
# Convert stamp to date
|
||||
ban["ban_start_date"] = datetime.fromtimestamp(floor(ban["date"])).strftime("%Y/%m/%d at %H:%M:%S %Z")
|
||||
ban["ban_end_date"] = datetime.fromtimestamp(floor(timestamp_now + exp)).strftime("%Y/%m/%d at %H:%M:%S %Z")
|
||||
reasons.add(ban["reason"])
|
||||
ban["start_date"] = datetime.fromtimestamp(floor(ban["date"])).astimezone().isoformat()
|
||||
ban["end_date"] = datetime.fromtimestamp(floor(timestamp_now + exp)).astimezone().isoformat()
|
||||
|
||||
# builder = bans_builder(bans, list(reasons), list(remains))
|
||||
# return render_template("bans.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("bans.html") # TODO
|
||||
return render_template("bans.html", bans=bans)
|
||||
|
||||
|
||||
@bans.route("/bans/ban", methods=["POST"])
|
||||
@login_required
|
||||
def bans_ban():
|
||||
verify_data_in_form(
|
||||
data={"bans": None},
|
||||
err_message="Missing bans parameter on /bans/ban.",
|
||||
redirect_url="bans",
|
||||
next=True,
|
||||
)
|
||||
bans = request.form["bans"]
|
||||
if not bans:
|
||||
return handle_error("No bans.", "bans", True)
|
||||
try:
|
||||
bans = loads(bans)
|
||||
except JSONDecodeError:
|
||||
return handle_error("Invalid bans parameter on /bans/ban.", "bans", True)
|
||||
|
||||
redis_client = get_redis_client()
|
||||
for ban in bans:
|
||||
if not isinstance(ban, dict) or "ip" not in ban:
|
||||
flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
reason = ban.get("reason", "ui")
|
||||
try:
|
||||
ban_end = datetime.fromisoformat(ban["end_date"])
|
||||
except ValueError:
|
||||
flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
continue
|
||||
current_time = datetime.now().astimezone()
|
||||
ban_end = (ban_end - current_time).total_seconds()
|
||||
|
||||
if redis_client:
|
||||
ok = redis_client.set(f"bans_ip_{ban['ip']}", dumps({"reason": reason, "date": time()}))
|
||||
if not ok:
|
||||
flash(f"Couldn't ban {ban['ip']} on redis", "error")
|
||||
redis_client.expire(f"bans_ip_{ban['ip']}", int(ban_end))
|
||||
|
||||
resp = BW_INSTANCES_UTILS.ban(ban["ip"], ban_end, reason)
|
||||
if resp:
|
||||
flash(f"Couldn't ban {ban['ip']} on the following instances: {', '.join(resp)}", "error")
|
||||
else:
|
||||
flash(f"Successfully banned {ban['ip']}")
|
||||
|
||||
return redirect(url_for("loading", next=url_for("bans.bans_page"), message=f"Banning {len(bans)} IP{'s' if len(bans) > 1 else ''}"))
|
||||
|
||||
|
||||
@bans.route("/bans/unban", methods=["POST"])
|
||||
@login_required
|
||||
def bans_unban():
|
||||
verify_data_in_form(
|
||||
data={"ips": None},
|
||||
err_message="Missing bans parameter on /bans/unban.",
|
||||
redirect_url="bans",
|
||||
next=True,
|
||||
)
|
||||
unbans = request.form["ips"]
|
||||
if not unbans:
|
||||
return handle_error("No bans.", "ips", True)
|
||||
try:
|
||||
unbans = loads(unbans)
|
||||
except JSONDecodeError:
|
||||
return handle_error("Invalid ips parameter on /bans/unban.", "bans", True)
|
||||
|
||||
redis_client = get_redis_client()
|
||||
for unban in unbans:
|
||||
if "ip" not in unban:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
if redis_client:
|
||||
if not redis_client.delete(f"bans_ip_{unban['ip']}"):
|
||||
flash(f"Couldn't unban {unban['ip']} on redis", "error")
|
||||
|
||||
resp = BW_INSTANCES_UTILS.unban(unban["ip"])
|
||||
if resp:
|
||||
flash(f"Couldn't unban {unban['ip']} on the following instances: {', '.join(resp)}", "error")
|
||||
else:
|
||||
flash(f"Successfully unbanned {unban['ip']}")
|
||||
|
||||
return redirect(url_for("loading", next=url_for("bans.bans_page"), message=f"Unbanning {len(unbans)} IP{'s' if len(unbans) > 1 else ''}"))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from io import BytesIO
|
|||
from flask import Blueprint, Response, flash, redirect, render_template, request, send_file, url_for
|
||||
from flask_login import login_required
|
||||
from magic import Magic
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.dependencies import DB
|
||||
|
||||
|
|
@ -21,7 +22,11 @@ def cache_page():
|
|||
@cache.route("/cache/<string:service>/<string:plugin_id>/<string:job_name>/<string:file_name>", methods=["GET"])
|
||||
@login_required
|
||||
def cache_view(service: str, plugin_id: str, job_name: str, file_name: str):
|
||||
file_name = file_name.replace("_", "/")
|
||||
if file_name.startswith("folder:"):
|
||||
file_name = file_name.replace("_", "/")
|
||||
else:
|
||||
file_name = secure_filename(file_name)
|
||||
|
||||
cache_file = DB.get_job_cache_file(
|
||||
job_name,
|
||||
file_name,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ from typing import Dict, Literal, Optional
|
|||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB # TODO: remember about DATA.load_from_file()
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
|
||||
from app.routes.utils import handle_error, verify_data_in_form, wait_applying
|
||||
|
||||
|
|
@ -61,18 +62,24 @@ def configs_delete():
|
|||
wait_applying()
|
||||
|
||||
db_configs = DB.get_custom_configs(with_drafts=True)
|
||||
new_db_configs = []
|
||||
configs_to_delete = set()
|
||||
non_ui_configs = set()
|
||||
|
||||
for i, db_config in enumerate(db_configs):
|
||||
for db_config in db_configs:
|
||||
key = f"{(db_config['service_id'] + '/') if db_config['service_id'] else ''}{db_config['type']}/{db_config['name']}"
|
||||
keep = True
|
||||
for config in configs:
|
||||
if db_config["name"] == config["name"] and db_config["service_id"] == config["service"] and db_config["type"] == config["type"]:
|
||||
key = f"{(config['service'] + '/') if config['service'] else ''}{config['type']}/{config['name']}"
|
||||
if db_config["method"] != "ui":
|
||||
non_ui_configs.add(key)
|
||||
continue
|
||||
configs_to_delete.add(key)
|
||||
del db_configs[i]
|
||||
keep = False
|
||||
break
|
||||
if db_config.pop("template", None) or not keep:
|
||||
continue
|
||||
new_db_configs.append(db_config)
|
||||
|
||||
for non_ui_config in non_ui_configs:
|
||||
DATA["TO_FLASH"].append(
|
||||
|
|
@ -87,7 +94,7 @@ def configs_delete():
|
|||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
error = DB.save_custom_configs(db_configs, "ui")
|
||||
error = DB.save_custom_configs(new_db_configs, "ui")
|
||||
if error:
|
||||
DATA["TO_FLASH"].append({"content": f"An error occurred while saving the custom configs: {error}", "type": "error"})
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
|
|
@ -118,7 +125,7 @@ def configs_new():
|
|||
next=True,
|
||||
)
|
||||
service = request.form["service"]
|
||||
services = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
services = BW_CONFIG.get_config(global_only=True, with_drafts=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if service != "no service" and service not in services:
|
||||
return handle_error(f"Service {service} does not exist.", "configs/new", True)
|
||||
|
||||
|
|
@ -162,32 +169,6 @@ def configs_new():
|
|||
wait_applying()
|
||||
config_type = config_type.lower()
|
||||
|
||||
db_configs = DB.get_custom_configs(with_drafts=True)
|
||||
for i, db_config in enumerate(db_configs.copy()):
|
||||
if db_config["method"] == "default" and db_config["template"]:
|
||||
del db_configs[i]
|
||||
continue
|
||||
|
||||
if db_config["type"] == config_type and db_config["name"] == config_name and db_config["service_id"] == service:
|
||||
DATA["TO_FLASH"].append(
|
||||
{
|
||||
"content": f"Config {config_type}/{config_name}{' for service ' + service if service else ''} already exists",
|
||||
"type": "error",
|
||||
}
|
||||
)
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
|
||||
db_configs[i] = {
|
||||
"service_id": db_config["service_id"],
|
||||
"type": db_config["type"],
|
||||
"name": db_config["name"],
|
||||
"data": db_config["data"],
|
||||
"method": db_config["method"],
|
||||
}
|
||||
if "checksum" in db_config:
|
||||
db_configs[i]["checksum"] = db_config["checksum"]
|
||||
|
||||
new_config = {
|
||||
"type": config_type,
|
||||
"name": config_name,
|
||||
|
|
@ -196,10 +177,18 @@ def configs_new():
|
|||
}
|
||||
if service != "no service":
|
||||
new_config["service_id"] = service
|
||||
db_configs.append(new_config)
|
||||
|
||||
error = DB.save_custom_configs(db_configs, "ui")
|
||||
error = DB.upsert_custom_config(config_type, config_name, new_config, service_id=new_config.get("service_id"), new=True)
|
||||
if error:
|
||||
if error == "The custom config already exists":
|
||||
DATA["TO_FLASH"].append(
|
||||
{
|
||||
"content": f"Config {config_type}/{config_name}{' for service ' + service if service else ''} already exists",
|
||||
"type": "error",
|
||||
}
|
||||
)
|
||||
DATA.update({"RELOADING": False, "CONFIG_CHANGED": False})
|
||||
return
|
||||
DATA["TO_FLASH"].append({"content": f"An error occurred while saving the custom configs: {error}", "type": "error"})
|
||||
return
|
||||
DATA["TO_FLASH"].append(
|
||||
|
|
@ -242,6 +231,7 @@ def configs_new():
|
|||
def configs_edit(service: str, config_type: str, name: str):
|
||||
if service == "global":
|
||||
service = None
|
||||
name = secure_filename(name)
|
||||
|
||||
db_config = DB.get_custom_config(config_type, name, service_id=service, with_data=True)
|
||||
if not db_config:
|
||||
|
|
@ -260,7 +250,7 @@ def configs_edit(service: str, config_type: str, name: str):
|
|||
next=True,
|
||||
)
|
||||
new_service = request.form["service"]
|
||||
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, with_drafts=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
if new_service != "no service" and new_service not in services:
|
||||
return handle_error(f"Service {new_service} does not exist.", "configs/new", True)
|
||||
|
||||
|
|
@ -284,7 +274,7 @@ def configs_edit(service: str, config_type: str, name: str):
|
|||
redirect_url="configs/new",
|
||||
next=True,
|
||||
)
|
||||
new_name = request.form["name"]
|
||||
new_name = secure_filename(request.form["name"])
|
||||
if not match(r"^[\w_-]{1,64}$", new_name):
|
||||
return handle_error("Invalid name parameter on /configs/new.", "configs/new", True)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
from io import BytesIO
|
||||
|
||||
from flask import Blueprint, jsonify, render_template, request, send_file
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.dependencies import DB
|
||||
|
||||
|
|
@ -12,26 +9,4 @@ jobs = Blueprint("jobs", __name__)
|
|||
@jobs.route("/jobs", methods=["GET"])
|
||||
@login_required
|
||||
def jobs_page():
|
||||
# builder = jobs_builder(DB.get_jobs())
|
||||
# return render_template("jobs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("jobs.html") # TODO
|
||||
|
||||
|
||||
@jobs.route("/jobs/download", methods=["GET"])
|
||||
@login_required
|
||||
def jobs_download():
|
||||
plugin_id = request.args.get("plugin_id", "")
|
||||
job_name = request.args.get("job_name", None)
|
||||
file_name = request.args.get("file_name", None)
|
||||
service_id = request.args.get("service_id", "")
|
||||
|
||||
if not plugin_id or not job_name or not file_name:
|
||||
return jsonify({"status": "ko", "message": "plugin_id, job_name and file_name are required"}), 422
|
||||
|
||||
cache_file = DB.get_job_cache_file(job_name, file_name, service_id=service_id, plugin_id=plugin_id)
|
||||
|
||||
if not cache_file:
|
||||
return jsonify({"status": "ko", "message": "file not found"}), 404
|
||||
|
||||
file = BytesIO(cache_file)
|
||||
return send_file(file, as_attachment=True, download_name=secure_filename(file_name))
|
||||
return render_template("jobs.html", jobs=DB.get_jobs())
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ def logs_page():
|
|||
files.append(file.name)
|
||||
|
||||
current_file = secure_filename(request.args.get("file", ""))
|
||||
page = request.args.get("page")
|
||||
|
||||
if current_file and current_file not in files:
|
||||
return Response("No such file", 404)
|
||||
|
|
@ -30,11 +31,14 @@ def logs_page():
|
|||
if isabs(current_file) or ".." in current_file:
|
||||
return error_message("Invalid file path", 400)
|
||||
|
||||
# raw_logs = ""
|
||||
# if current_file:
|
||||
# with logs_path.joinpath(current_file).open(encoding="utf-8") as f:
|
||||
# raw_logs = f.read()
|
||||
raw_logs = "Select a log file to view its contents"
|
||||
page_num = 1
|
||||
if current_file:
|
||||
with logs_path.joinpath(current_file).open(encoding="utf-8") as f:
|
||||
raw_logs = f.read().splitlines()
|
||||
page_num = len(raw_logs) // 10000 + 1
|
||||
if not page:
|
||||
page = page_num
|
||||
raw_logs = "\n".join(raw_logs[int(page) * 10000 - 10000 : int(page) * 10000]) # noqa: E203
|
||||
|
||||
# builder = logs_builder(files, current_file, raw_logs)
|
||||
# return render_template("logs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("logs.html") # TODO
|
||||
return render_template("logs.html", logs=raw_logs, files=files, current_file=current_file, current_page=int(page), page_num=page_num)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from math import floor
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
|
@ -12,25 +12,6 @@ reports = Blueprint("reports", __name__)
|
|||
@login_required
|
||||
def reports_page():
|
||||
reports = BW_INSTANCES_UTILS.get_reports()
|
||||
reasons = set()
|
||||
countries = set()
|
||||
methods = set()
|
||||
codes = set()
|
||||
|
||||
# Prepare data
|
||||
reports_items = []
|
||||
for i, report in enumerate(reports):
|
||||
report_item = {
|
||||
"id": str(i),
|
||||
"date": str(floor(report.pop("date"))),
|
||||
} | report
|
||||
reports_items.append(report_item)
|
||||
|
||||
reasons.add(report["reason"])
|
||||
countries.add(report["country"])
|
||||
methods.add(report["method"])
|
||||
codes.add(report["code"])
|
||||
|
||||
# builder = reports_builder(reports_items, list(reasons), list(countries), list(methods), list(codes))
|
||||
# return render_template("reports.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("reports.html")
|
||||
for i in range(len(reports)):
|
||||
reports[i]["date"] = datetime.fromtimestamp(reports[i]["date"]).astimezone().isoformat()
|
||||
return render_template("reports.html", reports=list(filter(lambda x: 400 <= x["status"] < 500, reports))) # TODO: check why we need to filter this
|
||||
|
|
|
|||
|
|
@ -365,63 +365,3 @@ def services_service_page(service: str):
|
|||
type=search_type,
|
||||
current_template=template,
|
||||
)
|
||||
|
||||
|
||||
# def update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged):
|
||||
# if request.form["operation"] == "edit":
|
||||
# if is_draft_unchanged and len(variables) == 1 and "SERVER_NAME" in variables and server_name == old_server_name:
|
||||
# return handle_error("The service was not edited because no values were changed.", "services", True)
|
||||
|
||||
# if request.form["operation"] == "new" and not variables:
|
||||
# return handle_error("The service was not created because all values had the default value.", "services", True)
|
||||
|
||||
# # Delete
|
||||
# if request.form["operation"] == "delete":
|
||||
|
||||
# is_service = BW_CONFIG.check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
|
||||
|
||||
# if not is_service:
|
||||
# error_message(f"Error while deleting the service {request.form['SERVER_NAME']}")
|
||||
|
||||
# if config.get(f"{request.form['SERVER_NAME'].split(' ')[0]}_SERVER_NAME", {"method": "scheduler"})["method"] != "ui":
|
||||
# return handle_error("The service cannot be deleted because it has not been created with the UI.", "services", True)
|
||||
|
||||
# db_metadata = DB.get_metadata()
|
||||
|
||||
# def update_services(threaded: bool = False):
|
||||
# wait_applying()
|
||||
|
||||
# manage_bunkerweb(
|
||||
# "services",
|
||||
# variables,
|
||||
# old_server_name,
|
||||
# variables.get("SERVER_NAME", ""),
|
||||
# operation=operation,
|
||||
# is_draft=is_draft,
|
||||
# was_draft=was_draft,
|
||||
# threaded=threaded,
|
||||
# )
|
||||
|
||||
# if any(
|
||||
# v
|
||||
# for k, v in db_metadata.items()
|
||||
# if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
# ):
|
||||
# DATA["RELOADING"] = True
|
||||
# DATA["LAST_RELOAD"] = time()
|
||||
# Thread(target=update_services, args=(True,)).start()
|
||||
# else:
|
||||
# update_services()
|
||||
|
||||
# DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
# message = ""
|
||||
|
||||
# if request.form["operation"] == "new":
|
||||
# message = f"Creating {'draft ' if is_draft else ''}service {variables.get('SERVER_NAME', '').split(' ')[0]}"
|
||||
# elif request.form["operation"] == "edit":
|
||||
# message = f"Saving configuration for {'draft ' if is_draft else ''}service {old_server_name.split(' ')[0]}"
|
||||
# elif request.form["operation"] == "delete":
|
||||
# message = f"Deleting {'draft ' if was_draft and is_draft else ''}service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
|
||||
|
||||
# return message
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import Any, Dict, Optional, Tuple, Union
|
|||
|
||||
from flask import Response, flash, redirect, request, session, url_for
|
||||
from qrcode.main import QRCode
|
||||
from redis import Redis, Sentinel
|
||||
from regex import compile as re_compile
|
||||
|
||||
from app.models.instance import Instance
|
||||
|
|
@ -320,3 +321,94 @@ def cors_required(f):
|
|||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def get_redis_client():
|
||||
redis_client = None
|
||||
db_config = BW_CONFIG.get_config(
|
||||
global_only=True,
|
||||
methods=False,
|
||||
filtered_settings=(
|
||||
"USE_REDIS",
|
||||
"REDIS_HOST",
|
||||
"REDIS_PORT",
|
||||
"REDIS_DB",
|
||||
"REDIS_TIMEOUT",
|
||||
"REDIS_KEEPALIVE_POOL",
|
||||
"REDIS_SSL",
|
||||
"REDIS_USERNAME",
|
||||
"REDIS_PASSWORD",
|
||||
"REDIS_SENTINEL_HOSTS",
|
||||
"REDIS_SENTINEL_USERNAME",
|
||||
"REDIS_SENTINEL_PASSWORD",
|
||||
"REDIS_SENTINEL_MASTER",
|
||||
),
|
||||
)
|
||||
use_redis = db_config.get("USE_REDIS", "no") == "yes"
|
||||
redis_host = db_config.get("REDIS_HOST")
|
||||
if use_redis and redis_host:
|
||||
redis_port = db_config.get("REDIS_PORT", "6379")
|
||||
if not redis_port.isdigit():
|
||||
redis_port = "6379"
|
||||
redis_port = int(redis_port)
|
||||
|
||||
redis_db = db_config.get("REDIS_DB", "0")
|
||||
if not redis_db.isdigit():
|
||||
redis_db = "0"
|
||||
redis_db = int(redis_db)
|
||||
|
||||
redis_timeout = db_config.get("REDIS_TIMEOUT", "1000.0")
|
||||
try:
|
||||
redis_timeout = float(redis_timeout)
|
||||
except ValueError:
|
||||
redis_timeout = 1000.0
|
||||
|
||||
redis_keepalive_pool = db_config.get("REDIS_KEEPALIVE_POOL", "10")
|
||||
if not redis_keepalive_pool.isdigit():
|
||||
redis_keepalive_pool = "10"
|
||||
redis_keepalive_pool = int(redis_keepalive_pool)
|
||||
|
||||
redis_ssl = db_config.get("REDIS_SSL", "no") == "yes"
|
||||
username = db_config.get("REDIS_USERNAME", None) or None
|
||||
password = db_config.get("REDIS_PASSWORD", None) or None
|
||||
sentinel_hosts = db_config.get("REDIS_SENTINEL_HOSTS", [])
|
||||
|
||||
if isinstance(sentinel_hosts, str):
|
||||
sentinel_hosts = [host.split(":") if ":" in host else (host, "26379") for host in sentinel_hosts.split(" ") if host]
|
||||
|
||||
if sentinel_hosts:
|
||||
sentinel_username = db_config.get("REDIS_SENTINEL_USERNAME", None) or None
|
||||
sentinel_password = db_config.get("REDIS_SENTINEL_PASSWORD", None) or None
|
||||
sentinel_master = db_config.get("REDIS_SENTINEL_MASTER", "")
|
||||
|
||||
sentinel = Sentinel(
|
||||
sentinel_hosts,
|
||||
username=sentinel_username,
|
||||
password=sentinel_password,
|
||||
ssl=redis_ssl,
|
||||
socket_timeout=redis_timeout,
|
||||
socket_connect_timeout=redis_timeout,
|
||||
socket_keepalive=True,
|
||||
max_connections=redis_keepalive_pool,
|
||||
)
|
||||
redis_client = sentinel.slave_for(sentinel_master, db=redis_db, username=username, password=password)
|
||||
else:
|
||||
redis_client = Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
db=redis_db,
|
||||
username=username,
|
||||
password=password,
|
||||
socket_timeout=redis_timeout,
|
||||
socket_connect_timeout=redis_timeout,
|
||||
socket_keepalive=True,
|
||||
max_connections=redis_keepalive_pool,
|
||||
ssl=redis_ssl,
|
||||
)
|
||||
|
||||
try:
|
||||
redis_client.ping()
|
||||
except BaseException:
|
||||
redis_client = None
|
||||
flash("Couldn't connect to redis, ban list might be incomplete", "error")
|
||||
return redis_client
|
||||
|
|
|
|||
563
src/ui/app/static/js/pages/bans.js
Normal file
563
src/ui/app/static/js/pages/bans.js
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
$(document).ready(function () {
|
||||
var actionLock = false;
|
||||
let addBanNumber = 1;
|
||||
const banNumber = parseInt($("#bans_number").val());
|
||||
|
||||
// Utility functions
|
||||
function addDays(date, days) {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
function addHours(date, hours) {
|
||||
const result = new Date(date);
|
||||
result.setHours(result.getHours() + hours);
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
let month = date.getMonth() + 1; // Months are zero-based in JavaScript
|
||||
let day = date.getDate();
|
||||
|
||||
// Pad month and day with leading zeros if needed
|
||||
month = month < 10 ? "0" + month : month;
|
||||
day = day < 10 ? "0" + day : day;
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
let hours = date.getHours();
|
||||
let minutes = date.getMinutes();
|
||||
|
||||
// Pad hours and minutes with leading zeros if needed
|
||||
hours = hours < 10 ? "0" + hours : hours;
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// Select the Flatpickr input elements
|
||||
const flatpickrDatetime = $("[type='flapickr-datetime']");
|
||||
|
||||
// Get the current date and times
|
||||
const currentDatetime = new Date();
|
||||
const minDatetime = addHours(currentDatetime, 1);
|
||||
const defaultDatetime = addDays(currentDatetime, 1);
|
||||
|
||||
// Format dates and times
|
||||
const minDateStr = formatDate(minDatetime);
|
||||
const minTimeStr = formatTime(minDatetime);
|
||||
|
||||
// Create the minMaxTime table
|
||||
const minMaxTable = {
|
||||
[minDateStr]: {
|
||||
minTime: minTimeStr,
|
||||
},
|
||||
};
|
||||
|
||||
const getTimeZoneOffset = () => {
|
||||
const offset = -currentDatetime.getTimezoneOffset(); // getTimezoneOffset returns minutes behind UTC
|
||||
const sign = offset >= 0 ? "+" : "-";
|
||||
const absOffset = Math.abs(offset);
|
||||
const hours = String(Math.floor(absOffset / 60)).padStart(2, "0");
|
||||
const minutes = String(absOffset % 60).padStart(2, "0");
|
||||
return `${sign}${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
// Initialize Flatpickr with altInput and altFormat
|
||||
const originalFlatpickr = flatpickrDatetime.flatpickr({
|
||||
enableTime: true,
|
||||
dateFormat: "Y-m-d\\TH:i:S", // ISO format
|
||||
altInput: true,
|
||||
altFormat: "F j, Y h:i", // User-friendly display format
|
||||
time_24hr: true,
|
||||
defaultDate: defaultDatetime,
|
||||
minDate: minDatetime,
|
||||
plugins: [
|
||||
new minMaxTimePlugin({
|
||||
table: minMaxTable,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const setupUnbanModal = (bans) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">IP Address</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Time left</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-ips-unban").append(list);
|
||||
|
||||
bans.forEach((ban) => {
|
||||
// Create the list item using template literals
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
const listItem =
|
||||
$(`<li class="list-group-item align-items-center" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${ban.ip}</div>
|
||||
</div>
|
||||
</li>`);
|
||||
list.append(listItem);
|
||||
|
||||
const timeLeft =
|
||||
$(`<li class="list-group-item align-items-center" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
${ban.time_remaining}
|
||||
</div>
|
||||
</li>`);
|
||||
list.append(timeLeft);
|
||||
|
||||
$("#selected-ips-unban").append(list);
|
||||
});
|
||||
|
||||
const unban_modal = $("#modal-unban-ips");
|
||||
const modal = new bootstrap.Modal(unban_modal);
|
||||
unban_modal
|
||||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to unban the selected IP address${"es".repeat(
|
||||
bans.length > 1,
|
||||
)}?`,
|
||||
);
|
||||
modal.show();
|
||||
|
||||
$("#selected-ips-input-unban").val(JSON.stringify(bans));
|
||||
};
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
};
|
||||
|
||||
if (banNumber > 10) {
|
||||
layout.topStart.pageLength = {
|
||||
menu: [10, 25, 50, 100, { label: "All", value: -1 }],
|
||||
};
|
||||
layout.bottomEnd.paging = true;
|
||||
}
|
||||
|
||||
layout.topStart.buttons = [
|
||||
{
|
||||
extend: "colvis",
|
||||
columns: "th:not(:first-child):not(:nth-child(2)):not(:last-child)",
|
||||
text: '<span class="tf-icons bx bx-columns bx-18px me-2"></span>Columns',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
columnText: function (dt, idx, title) {
|
||||
return idx + 1 + ". " + title;
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "colvisRestore",
|
||||
text: '<span class="tf-icons bx bx-reset bx-18px me-2"></span>Reset<span class="d-none d-md-inline"> columns</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
},
|
||||
{
|
||||
extend: "collection",
|
||||
text: '<span class="tf-icons bx bx-export bx-18px me-2"></span>Export',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
buttons: [
|
||||
{
|
||||
extend: "copy",
|
||||
text: '<span class="tf-icons bx bx-copy bx-18px me-2"></span>Copy current page',
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
page: "current",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "csv",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>CSV',
|
||||
bom: true,
|
||||
filename: "bw_bans",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "excel",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>Excel',
|
||||
filename: "bw_bans",
|
||||
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: "unban_ips",
|
||||
className: "text-danger",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
extend: "add_ban",
|
||||
},
|
||||
];
|
||||
|
||||
$("#modal-unban-ips").on("hidden.bs.modal", function () {
|
||||
$("#selected-ips-unban").empty();
|
||||
$("#selected-ips-input-unban").val("");
|
||||
});
|
||||
|
||||
const getSelectedBans = () => {
|
||||
const bans = [];
|
||||
$("tr.selected").each(function () {
|
||||
const ip = $(this).find("td:eq(2)").text().trim();
|
||||
const time_remaining = $(this).find("td:eq(5)").text().trim();
|
||||
bans.push({ ip: ip, time_remaining: time_remaining });
|
||||
});
|
||||
return bans;
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.add_ban = {
|
||||
text: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Add<span class="d-none d-md-inline"> ban(s)</span>',
|
||||
className: "btn btn-sm btn-outline-bw-green",
|
||||
action: function (e, dt, node, config) {
|
||||
const ban_modal = $("#modal-ban-ips");
|
||||
const modal = new bootstrap.Modal(ban_modal);
|
||||
modal.show();
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.unban_ips = {
|
||||
text: '<span class="tf-icons bx bxs-buoy bx-18px me-2"></span>Unban',
|
||||
action: function (e, dt, node, config) {
|
||||
if (actionLock) {
|
||||
return;
|
||||
}
|
||||
actionLock = true;
|
||||
$(".dt-button-background").click();
|
||||
|
||||
const bans = getSelectedBans();
|
||||
if (bans.length === 0) {
|
||||
actionLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
setupUnbanModal(bans);
|
||||
|
||||
actionLock = false;
|
||||
},
|
||||
};
|
||||
|
||||
$(".ban-start-date, .ban-end-date").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
} else {
|
||||
// Handle invalid date
|
||||
console.error(`Invalid date string: ${isoDateStr}`);
|
||||
$(this).text("Invalid date");
|
||||
}
|
||||
});
|
||||
|
||||
const bans_table = new DataTable("#bans", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[4, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:first-child",
|
||||
headerCheckbox: false,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ bans",
|
||||
infoEmpty: "No bans available",
|
||||
infoFiltered: "(filtered from _MAX_ total bans)",
|
||||
lengthMenu: "Display _MENU_ bans",
|
||||
zeroRecords: "No matching bans found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d bans",
|
||||
0: "No bans selected",
|
||||
1: "Selected 1 ban",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#bans_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#bans_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
bans_table.on("mouseenter", "td", function () {
|
||||
if (bans_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = bans_table.cell(this).index().row;
|
||||
|
||||
bans_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
bans_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (bans_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
bans_table.on("mouseleave", "td", function () {
|
||||
bans_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
|
||||
bans_table.rows({ page: "current" }).select();
|
||||
} else {
|
||||
// Deselect all rows on the current page
|
||||
bans_table.rows({ page: "current" }).deselect();
|
||||
}
|
||||
});
|
||||
|
||||
$(".unban-ip").on("click", function () {
|
||||
$this = $(this);
|
||||
setupUnbanModal([
|
||||
{ ip: $this.data("ip"), time_remaining: $this.data("time-left") },
|
||||
]);
|
||||
});
|
||||
|
||||
$("#add-ban").on("click", function () {
|
||||
const originalBan = $("#ban-1");
|
||||
const banClone = originalBan.clone();
|
||||
banClone.attr("id", `ban-${++addBanNumber}`);
|
||||
|
||||
banClone
|
||||
.find("input[name='ip']")
|
||||
.removeClass("is-valid is-invalid")
|
||||
.val("");
|
||||
banClone.find("[readonly='readonly']").remove();
|
||||
banClone.find(".flatpickr-input").flatpickr({
|
||||
enableTime: true,
|
||||
dateFormat: "Y-m-d\\TH:i:S", // ISO format
|
||||
altInput: true,
|
||||
altFormat: "F j, Y h:i", // User-friendly display format
|
||||
time_24hr: true,
|
||||
defaultDate: defaultDatetime,
|
||||
minDate: minDatetime,
|
||||
plugins: [
|
||||
new minMaxTimePlugin({
|
||||
table: minMaxTable,
|
||||
}),
|
||||
],
|
||||
});
|
||||
banClone.find("input[name='reason']").val("ui");
|
||||
|
||||
const deleteButton = banClone.find(".delete-ban");
|
||||
deleteButton.removeClass("disabled");
|
||||
|
||||
$("#bans-container").append(banClone);
|
||||
});
|
||||
|
||||
$("#clear-bans").on("click", function () {
|
||||
$("#bans-container")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
if (
|
||||
$(this).attr("id") === "ban-1" ||
|
||||
$(this).attr("id") === "bans-header"
|
||||
)
|
||||
return;
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("click", ".delete-ban", function () {
|
||||
const banContainer = $(this).closest("li");
|
||||
if (banContainer.attr("id") === "ban-1") return;
|
||||
banContainer.remove();
|
||||
});
|
||||
|
||||
$("#modal-ban-ips").on("hidden.bs.modal", function () {
|
||||
$("#clear-bans").trigger("click");
|
||||
const firstBan = $("#ban-1");
|
||||
firstBan.find("input[name='ip']").val("");
|
||||
firstBan.find("input[name='reason']").val("ui");
|
||||
originalFlatpickr.setDate(defaultDatetime);
|
||||
});
|
||||
|
||||
const ipRegex = new RegExp(
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?!$)|$)){4}$|^((?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|:(?::[A-Fa-f0-9]{1,4}){1,7}|::)$/i,
|
||||
);
|
||||
|
||||
const validateBan = (ban, ipSet) => {
|
||||
const value = ban.val().trim();
|
||||
let errorMessage = "";
|
||||
let isValid = true;
|
||||
|
||||
if (value === "") {
|
||||
errorMessage = "Please enter an IP address.";
|
||||
isValid = false;
|
||||
} else if (!ipRegex.test(value)) {
|
||||
errorMessage = "Please enter a valid IP address.";
|
||||
isValid = false;
|
||||
} else if (ipSet.has(value)) {
|
||||
errorMessage = "This IP address has already been entered.";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Toggle valid/invalid classes
|
||||
ban.toggleClass("is-valid", isValid).toggleClass("is-invalid", !isValid);
|
||||
|
||||
// Manage the invalid-feedback element
|
||||
let $feedback = ban.next(".invalid-feedback");
|
||||
if (!$feedback.length) {
|
||||
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(ban);
|
||||
}
|
||||
|
||||
$feedback.text(errorMessage);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
$("#bans-container").on("input", "input[name='ip']", function () {
|
||||
debounce(() => {
|
||||
const $input = $(this);
|
||||
const ipSet = new Set();
|
||||
|
||||
// Gather all other IPs except the current one
|
||||
$("#bans-container")
|
||||
.find("input[name='ip']")
|
||||
.not($input)
|
||||
.each(function () {
|
||||
ipSet.add($(this).val().trim());
|
||||
});
|
||||
|
||||
validateBan($input, ipSet);
|
||||
}, 100)();
|
||||
});
|
||||
|
||||
$("#bans-container").on("focusout", "input[name='ip']", function () {
|
||||
$(this).removeClass("is-valid");
|
||||
});
|
||||
|
||||
$("#bans-form").on("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
let allValid = true;
|
||||
const ipSet = new Set();
|
||||
|
||||
$("#bans-container")
|
||||
.find("input[name='ip']")
|
||||
.each(function () {
|
||||
const $input = $(this);
|
||||
const ip = $input.val().trim();
|
||||
|
||||
// Validate and check for duplicates
|
||||
if (!validateBan($input, ipSet)) {
|
||||
allValid = false;
|
||||
} else {
|
||||
ipSet.add(ip);
|
||||
}
|
||||
});
|
||||
|
||||
if (!allValid) return;
|
||||
|
||||
const bans = [];
|
||||
$("#bans-container")
|
||||
.find("li.rounded-0")
|
||||
.each(function () {
|
||||
$this = $(this);
|
||||
const ip = $this.find("input[name='ip']").val().trim();
|
||||
const end_date = $this.find(".flatpickr-input").val();
|
||||
const reason = $this.find("input[name='reason']").val().trim();
|
||||
bans.push({
|
||||
ip: ip,
|
||||
end_date: `${end_date}${getTimeZoneOffset()}`,
|
||||
reason: reason,
|
||||
});
|
||||
});
|
||||
|
||||
console.log(bans);
|
||||
const form = $("<form>", {
|
||||
method: "POST",
|
||||
action: `${window.location.pathname}/ban`,
|
||||
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: "bans",
|
||||
value: JSON.stringify(bans),
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
form.appendTo("body").submit();
|
||||
});
|
||||
});
|
||||
|
|
@ -67,6 +67,22 @@ $(document).ready(function () {
|
|||
},
|
||||
];
|
||||
|
||||
$(".cache-last-update-date").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
}
|
||||
});
|
||||
|
||||
const cache_table = new DataTable("#cache", {
|
||||
columnDefs: [
|
||||
{
|
||||
|
|
@ -80,7 +96,7 @@ $(document).ready(function () {
|
|||
{
|
||||
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>
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -94,20 +110,15 @@ $(document).ready(function () {
|
|||
infoFiltered: "(filtered from _MAX_ total cache files)",
|
||||
lengthMenu: "Display _MENU_ cache files",
|
||||
zeroRecords: "No matching cache files found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d cache files",
|
||||
0: "No cache files selected",
|
||||
1: "Selected 1 cache file",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#cache_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#cache_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
cache_table.on("mouseenter", "td", function () {
|
||||
if (cache_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = cache_table.cell(this).index().row;
|
||||
|
||||
cache_table
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ $(document).ready(function () {
|
|||
const delete_modal = $("#modal-delete-configs");
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Name</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="fw-bold">Type</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="fw-bold">Service</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
|
|
@ -28,7 +28,7 @@ $(document).ready(function () {
|
|||
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item align-items-center text-center" style="flex: 1 1 0;">
|
||||
$(`<li class="list-group-item align-items-center" style="flex: 1 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${config.name}</div>
|
||||
</div>
|
||||
|
|
@ -235,11 +235,11 @@ $(document).ready(function () {
|
|||
{
|
||||
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>
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[1, "desc"]],
|
||||
order: [[1, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
|
|
@ -264,10 +264,12 @@ $(document).ready(function () {
|
|||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#configs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#configs_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
configs_table.on("mouseenter", "td", function () {
|
||||
if (configs_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = configs_table.cell(this).index().row;
|
||||
|
||||
configs_table
|
||||
|
|
|
|||
|
|
@ -278,12 +278,12 @@ $(document).ready(function () {
|
|||
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Hostname</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Health</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
|
|
@ -298,7 +298,7 @@ $(document).ready(function () {
|
|||
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item align-items-center text-center" style="flex: 1 0;">
|
||||
$(`<li class="list-group-item align-items-center" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${instance}</div>
|
||||
</div>
|
||||
|
|
@ -324,6 +324,26 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
$(".instance-creation-date, .instance-last-seen-date").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
} else {
|
||||
// Handle invalid date
|
||||
console.error(`Invalid date string: ${isoDateStr}`);
|
||||
$(this).text("Invalid date");
|
||||
}
|
||||
});
|
||||
|
||||
const instances_table = new DataTable("#instances", {
|
||||
columnDefs: [
|
||||
{
|
||||
|
|
@ -342,7 +362,7 @@ $(document).ready(function () {
|
|||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("text-center"); // Apply 'text-center' class to <td>
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -371,10 +391,12 @@ $(document).ready(function () {
|
|||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#instances_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#instances_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
instances_table.on("mouseenter", "td", function () {
|
||||
if (instances_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = instances_table.cell(this).index().row;
|
||||
|
||||
instances_table
|
||||
|
|
|
|||
158
src/ui/app/static/js/pages/jobs.js
Normal file
158
src/ui/app/static/js/pages/jobs.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
$(document).ready(function () {
|
||||
const jobNumber = parseInt($("#job_number").val());
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
};
|
||||
|
||||
if (jobNumber > 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)",
|
||||
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_jobs",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "excel",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>Excel',
|
||||
filename: "bw_jobs",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
$(".history-start-date, .history-end-date").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
} else {
|
||||
// Handle invalid date
|
||||
console.error(`Invalid date string: ${isoDateStr}`);
|
||||
$(this).text("Invalid date");
|
||||
}
|
||||
});
|
||||
|
||||
const jobs_table = new DataTable("#jobs", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[1, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ jobs",
|
||||
infoEmpty: "No jobs available",
|
||||
infoFiltered: "(filtered from _MAX_ total jobs)",
|
||||
lengthMenu: "Display _MENU_ jobs",
|
||||
zeroRecords: "No matching jobs found",
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#jobs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#jobs_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
jobs_table.on("mouseenter", "td", function () {
|
||||
if (jobs_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = jobs_table.cell(this).index().row;
|
||||
|
||||
jobs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
jobs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (jobs_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
jobs_table.on("mouseleave", "td", function () {
|
||||
jobs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
$(".show-history").on("click", function () {
|
||||
const historyModal = $("#modal-job-history");
|
||||
const job = $(this).data("job");
|
||||
const plugin = $(this).data("plugin");
|
||||
|
||||
historyModal.find(".modal-title").text(`Job ${job} History`);
|
||||
const history = $(`#job-${job}-${plugin}-history`).clone();
|
||||
history.removeClass("visually-hidden");
|
||||
historyModal.find(".modal-body").html(history);
|
||||
|
||||
const modal = new bootstrap.Modal(historyModal);
|
||||
modal.show();
|
||||
});
|
||||
});
|
||||
43
src/ui/app/static/js/pages/logs.js
Normal file
43
src/ui/app/static/js/pages/logs.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
$(document).ready(function () {
|
||||
const editorElement = $("#raw-logs");
|
||||
const initialContent = editorElement.text().trim();
|
||||
const editor = ace.edit(editorElement[0]);
|
||||
editor.setTheme("ace/theme/cloud9_day"); // cloud9_night when dark mode is supported
|
||||
editor.session.setMode("ace/mode/text");
|
||||
editor.setReadOnly(true);
|
||||
|
||||
// Set the editor's initial content
|
||||
editor.setValue(initialContent, -1); // The second parameter moves the cursor to the start
|
||||
|
||||
editor.setOptions({
|
||||
fontSize: "14px",
|
||||
showPrintMargin: false,
|
||||
tabSize: 2,
|
||||
useSoftTabs: true,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
|
||||
$("#copy-logs").click(function () {
|
||||
$this = $(this);
|
||||
editor.selectAll();
|
||||
editor.focus();
|
||||
navigator.clipboard
|
||||
.writeText(editor.getSelectedText())
|
||||
.then(() => {
|
||||
// Success
|
||||
console.log("Copied to clipboard");
|
||||
})
|
||||
.catch((err) => {
|
||||
// Error
|
||||
console.error("Failed to copy to clipboard", err);
|
||||
});
|
||||
$this.attr("data-bs-original-title", "Copied!").tooltip("show");
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
$this.tooltip("hide").attr("data-bs-original-title", "");
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
151
src/ui/app/static/js/pages/reports.js
Normal file
151
src/ui/app/static/js/pages/reports.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
$(document).ready(function () {
|
||||
const reportsNumber = parseInt($("#reports_number").val());
|
||||
|
||||
const layout = {
|
||||
topStart: {},
|
||||
bottomEnd: {},
|
||||
};
|
||||
|
||||
if (reportsNumber > 10) {
|
||||
layout.topStart.pageLength = {
|
||||
menu: [10, 25, 50, 100, { label: "All", value: -1 }],
|
||||
};
|
||||
layout.bottomEnd.paging = true;
|
||||
}
|
||||
|
||||
layout.topStart.buttons = [
|
||||
{
|
||||
extend: "colvis",
|
||||
columns: "th:not(:first-child):not(:last-child)",
|
||||
text: '<span class="tf-icons bx bx-columns bx-18px me-2"></span>Columns',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
columnText: function (dt, idx, title) {
|
||||
return idx + 1 + ". " + title;
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "colvisRestore",
|
||||
text: '<span class="tf-icons bx bx-reset bx-18px me-2"></span>Reset<span class="d-none d-md-inline"> columns</span>',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
},
|
||||
{
|
||||
extend: "collection",
|
||||
text: '<span class="tf-icons bx bx-export bx-18px me-2"></span>Export',
|
||||
className: "btn btn-sm btn-outline-primary",
|
||||
buttons: [
|
||||
{
|
||||
extend: "copy",
|
||||
text: '<span class="tf-icons bx bx-copy bx-18px me-2"></span>Copy current page',
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
page: "current",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "csv",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>CSV',
|
||||
bom: true,
|
||||
filename: "bw_report",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extend: "excel",
|
||||
text: '<span class="tf-icons bx bx-table bx-18px me-2"></span>Excel',
|
||||
filename: "bw_report",
|
||||
exportOptions: {
|
||||
modifier: {
|
||||
search: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
$(".report-date").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
}
|
||||
});
|
||||
|
||||
const reports_table = new DataTable("#reports", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: [6, -1],
|
||||
},
|
||||
{
|
||||
targets: "_all", // Target all columns
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
order: [[0, "desc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ reports",
|
||||
infoEmpty: "No reports available",
|
||||
infoFiltered: "(filtered from _MAX_ total reports)",
|
||||
lengthMenu: "Display _MENU_ reports",
|
||||
zeroRecords: "No matching reports found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d reports",
|
||||
0: "No reports selected",
|
||||
1: "Selected 1 report",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#reports_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#reports_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
reports_table.on("mouseenter", "td", function () {
|
||||
if (reports_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = reports_table.cell(this).index().row;
|
||||
|
||||
reports_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
reports_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (reports_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
reports_table.on("mouseleave", "td", function () {
|
||||
reports_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
});
|
||||
|
|
@ -6,12 +6,12 @@ $(document).ready(function () {
|
|||
const setupModal = (services, modal) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Service name</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Type</div>
|
||||
</li>
|
||||
</ul>`,
|
||||
|
|
@ -25,7 +25,7 @@ $(document).ready(function () {
|
|||
|
||||
// Create the list item using template literals
|
||||
const listItem =
|
||||
$(`<li class="list-group-item align-items-center text-center" style="flex: 1 0;">
|
||||
$(`<li class="list-group-item align-items-center" style="flex: 1 0;">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">${service}</div>
|
||||
</div>
|
||||
|
|
@ -266,6 +266,26 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
$(".service-creation-date, .service-last-update-date").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
} else {
|
||||
// Handle invalid date
|
||||
console.error(`Invalid date string: ${isoDateStr}`);
|
||||
$(this).text("Invalid date");
|
||||
}
|
||||
});
|
||||
|
||||
const services_table = new DataTable("#services", {
|
||||
columnDefs: [
|
||||
{
|
||||
|
|
@ -280,7 +300,7 @@ $(document).ready(function () {
|
|||
{
|
||||
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>
|
||||
$(td).addClass("align-items-center"); // Apply 'text-center' class to <td>
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -309,10 +329,12 @@ $(document).ready(function () {
|
|||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#services_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
$("#services_wrapper th").addClass("text-center");
|
||||
},
|
||||
});
|
||||
|
||||
services_table.on("mouseenter", "td", function () {
|
||||
if (services_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = services_table.cell(this).index().row;
|
||||
|
||||
services_table
|
||||
|
|
|
|||
|
|
@ -682,6 +682,9 @@ $(document).ready(() => {
|
|||
</div>
|
||||
`);
|
||||
|
||||
multipleShow.html(`<i class="bx bx-hide bx-sm"></i> SHOW`);
|
||||
multipleClone.find(".multiple-collapse").collapse("hide");
|
||||
|
||||
// Insert the new element in the correct order based on suffix
|
||||
let inserted = false;
|
||||
existingContainers.each(function () {
|
||||
|
|
@ -698,9 +701,6 @@ $(document).ready(() => {
|
|||
}
|
||||
});
|
||||
|
||||
multipleShow.html(`<i class="bx bx-hide bx-sm"></i> SHOW`);
|
||||
multipleClone.find(".multiple-collapse").collapse("hide");
|
||||
|
||||
if (!inserted) {
|
||||
// If no higher suffix was found, append to the end
|
||||
$(`#${multipleId}`).append(multipleClone);
|
||||
|
|
@ -718,7 +718,7 @@ $(document).ready(() => {
|
|||
setTimeout(() => {
|
||||
showMultiple.trigger("click");
|
||||
highlightSettings(multipleClone);
|
||||
}, 50);
|
||||
}, 30);
|
||||
});
|
||||
|
||||
$(document).on("click", ".remove-multiple", function () {
|
||||
|
|
|
|||
13
src/ui/app/static/libs/flatpickr/flatpickr.min.css
vendored
Normal file
13
src/ui/app/static/libs/flatpickr/flatpickr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
src/ui/app/static/libs/flatpickr/flatpickr.min.js
vendored
Normal file
2
src/ui/app/static/libs/flatpickr/flatpickr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
349
src/ui/app/static/libs/flatpickr/plugins/minMaxTimePlugin.js
Normal file
349
src/ui/app/static/libs/flatpickr/plugins/minMaxTimePlugin.js
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.minMaxTimePlugin = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
var pad = function (number, length) {
|
||||
if (length === void 0) { length = 2; }
|
||||
return ("000" + number).slice(length * -1);
|
||||
};
|
||||
var int = function (bool) { return (bool === true ? 1 : 0); };
|
||||
|
||||
var monthToStr = function (monthNumber, shorthand, locale) { return locale.months[shorthand ? "shorthand" : "longhand"][monthNumber]; };
|
||||
var formats = {
|
||||
// get the date in UTC
|
||||
Z: function (date) { return date.toISOString(); },
|
||||
// weekday name, short, e.g. Thu
|
||||
D: function (date, locale, options) {
|
||||
return locale.weekdays.shorthand[formats.w(date, locale, options)];
|
||||
},
|
||||
// full month name e.g. January
|
||||
F: function (date, locale, options) {
|
||||
return monthToStr(formats.n(date, locale, options) - 1, false, locale);
|
||||
},
|
||||
// padded hour 1-12
|
||||
G: function (date, locale, options) {
|
||||
return pad(formats.h(date, locale, options));
|
||||
},
|
||||
// hours with leading zero e.g. 03
|
||||
H: function (date) { return pad(date.getHours()); },
|
||||
// day (1-30) with ordinal suffix e.g. 1st, 2nd
|
||||
J: function (date, locale) {
|
||||
return locale.ordinal !== undefined
|
||||
? date.getDate() + locale.ordinal(date.getDate())
|
||||
: date.getDate();
|
||||
},
|
||||
// AM/PM
|
||||
K: function (date, locale) { return locale.amPM[int(date.getHours() > 11)]; },
|
||||
// shorthand month e.g. Jan, Sep, Oct, etc
|
||||
M: function (date, locale) {
|
||||
return monthToStr(date.getMonth(), true, locale);
|
||||
},
|
||||
// seconds 00-59
|
||||
S: function (date) { return pad(date.getSeconds()); },
|
||||
// unix timestamp
|
||||
U: function (date) { return date.getTime() / 1000; },
|
||||
W: function (date, _, options) {
|
||||
return options.getWeek(date);
|
||||
},
|
||||
// full year e.g. 2016, padded (0001-9999)
|
||||
Y: function (date) { return pad(date.getFullYear(), 4); },
|
||||
// day in month, padded (01-30)
|
||||
d: function (date) { return pad(date.getDate()); },
|
||||
// hour from 1-12 (am/pm)
|
||||
h: function (date) { return (date.getHours() % 12 ? date.getHours() % 12 : 12); },
|
||||
// minutes, padded with leading zero e.g. 09
|
||||
i: function (date) { return pad(date.getMinutes()); },
|
||||
// day in month (1-30)
|
||||
j: function (date) { return date.getDate(); },
|
||||
// weekday name, full, e.g. Thursday
|
||||
l: function (date, locale) {
|
||||
return locale.weekdays.longhand[date.getDay()];
|
||||
},
|
||||
// padded month number (01-12)
|
||||
m: function (date) { return pad(date.getMonth() + 1); },
|
||||
// the month number (1-12)
|
||||
n: function (date) { return date.getMonth() + 1; },
|
||||
// seconds 0-59
|
||||
s: function (date) { return date.getSeconds(); },
|
||||
// Unix Milliseconds
|
||||
u: function (date) { return date.getTime(); },
|
||||
// number of the day of the week
|
||||
w: function (date) { return date.getDay(); },
|
||||
// last two digits of year e.g. 16 for 2016
|
||||
y: function (date) { return String(date.getFullYear()).substring(2); },
|
||||
};
|
||||
|
||||
var defaults = {
|
||||
_disable: [],
|
||||
allowInput: false,
|
||||
allowInvalidPreload: false,
|
||||
altFormat: "F j, Y",
|
||||
altInput: false,
|
||||
altInputClass: "form-control input",
|
||||
animate: typeof window === "object" &&
|
||||
window.navigator.userAgent.indexOf("MSIE") === -1,
|
||||
ariaDateFormat: "F j, Y",
|
||||
autoFillDefaultTime: true,
|
||||
clickOpens: true,
|
||||
closeOnSelect: true,
|
||||
conjunction: ", ",
|
||||
dateFormat: "Y-m-d",
|
||||
defaultHour: 12,
|
||||
defaultMinute: 0,
|
||||
defaultSeconds: 0,
|
||||
disable: [],
|
||||
disableMobile: false,
|
||||
enableSeconds: false,
|
||||
enableTime: false,
|
||||
errorHandler: function (err) {
|
||||
return typeof console !== "undefined" && console.warn(err);
|
||||
},
|
||||
getWeek: function (givenDate) {
|
||||
var date = new Date(givenDate.getTime());
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// Thursday in current week decides the year.
|
||||
date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7));
|
||||
// January 4 is always in week 1.
|
||||
var week1 = new Date(date.getFullYear(), 0, 4);
|
||||
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
|
||||
return (1 +
|
||||
Math.round(((date.getTime() - week1.getTime()) / 86400000 -
|
||||
3 +
|
||||
((week1.getDay() + 6) % 7)) /
|
||||
7));
|
||||
},
|
||||
hourIncrement: 1,
|
||||
ignoredFocusElements: [],
|
||||
inline: false,
|
||||
locale: "default",
|
||||
minuteIncrement: 5,
|
||||
mode: "single",
|
||||
monthSelectorType: "dropdown",
|
||||
nextArrow: "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 17 17'><g></g><path d='M13.207 8.472l-7.854 7.854-0.707-0.707 7.146-7.146-7.146-7.148 0.707-0.707 7.854 7.854z' /></svg>",
|
||||
noCalendar: false,
|
||||
now: new Date(),
|
||||
onChange: [],
|
||||
onClose: [],
|
||||
onDayCreate: [],
|
||||
onDestroy: [],
|
||||
onKeyDown: [],
|
||||
onMonthChange: [],
|
||||
onOpen: [],
|
||||
onParseConfig: [],
|
||||
onReady: [],
|
||||
onValueUpdate: [],
|
||||
onYearChange: [],
|
||||
onPreCalendarPosition: [],
|
||||
plugins: [],
|
||||
position: "auto",
|
||||
positionElement: undefined,
|
||||
prevArrow: "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 17 17'><g></g><path d='M5.207 8.471l7.146 7.147-0.707 0.707-7.853-7.854 7.854-7.853 0.707 0.707-7.147 7.146z' /></svg>",
|
||||
shorthandCurrentMonth: false,
|
||||
showMonths: 1,
|
||||
static: false,
|
||||
time_24hr: false,
|
||||
weekNumbers: false,
|
||||
wrap: false,
|
||||
};
|
||||
|
||||
var english = {
|
||||
weekdays: {
|
||||
shorthand: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
longhand: [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
],
|
||||
},
|
||||
months: {
|
||||
shorthand: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
longhand: [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
],
|
||||
},
|
||||
daysInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
|
||||
firstDayOfWeek: 0,
|
||||
ordinal: function (nth) {
|
||||
var s = nth % 100;
|
||||
if (s > 3 && s < 21)
|
||||
return "th";
|
||||
switch (s % 10) {
|
||||
case 1:
|
||||
return "st";
|
||||
case 2:
|
||||
return "nd";
|
||||
case 3:
|
||||
return "rd";
|
||||
default:
|
||||
return "th";
|
||||
}
|
||||
},
|
||||
rangeSeparator: " to ",
|
||||
weekAbbreviation: "Wk",
|
||||
scrollTitle: "Scroll to increment",
|
||||
toggleTitle: "Click to toggle",
|
||||
amPM: ["AM", "PM"],
|
||||
yearAriaLabel: "Year",
|
||||
monthAriaLabel: "Month",
|
||||
hourAriaLabel: "Hour",
|
||||
minuteAriaLabel: "Minute",
|
||||
time_24hr: false,
|
||||
};
|
||||
|
||||
var createDateFormatter = function (_a) {
|
||||
var _b = _a.config, config = _b === void 0 ? defaults : _b, _c = _a.l10n, l10n = _c === void 0 ? english : _c, _d = _a.isMobile, isMobile = _d === void 0 ? false : _d;
|
||||
return function (dateObj, frmt, overrideLocale) {
|
||||
var locale = overrideLocale || l10n;
|
||||
if (config.formatDate !== undefined && !isMobile) {
|
||||
return config.formatDate(dateObj, frmt, locale);
|
||||
}
|
||||
return frmt
|
||||
.split("")
|
||||
.map(function (c, i, arr) {
|
||||
return formats[c] && arr[i - 1] !== "\\"
|
||||
? formats[c](dateObj, locale, config)
|
||||
: c !== "\\"
|
||||
? c
|
||||
: "";
|
||||
})
|
||||
.join("");
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Compute the difference in dates, measured in ms
|
||||
*/
|
||||
function compareDates(date1, date2, timeless) {
|
||||
if (timeless === void 0) { timeless = true; }
|
||||
if (timeless !== false) {
|
||||
return (new Date(date1.getTime()).setHours(0, 0, 0, 0) -
|
||||
new Date(date2.getTime()).setHours(0, 0, 0, 0));
|
||||
}
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
/**
|
||||
* Compute the difference in times, measured in ms
|
||||
*/
|
||||
function compareTimes(date1, date2) {
|
||||
return (3600 * (date1.getHours() - date2.getHours()) +
|
||||
60 * (date1.getMinutes() - date2.getMinutes()) +
|
||||
date1.getSeconds() -
|
||||
date2.getSeconds());
|
||||
}
|
||||
var calculateSecondsSinceMidnight = function (hours, minutes, seconds) {
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
};
|
||||
var parseSeconds = function (secondsSinceMidnight) {
|
||||
var hours = Math.floor(secondsSinceMidnight / 3600), minutes = (secondsSinceMidnight - hours * 3600) / 60;
|
||||
return [hours, minutes, secondsSinceMidnight - hours * 3600 - minutes * 60];
|
||||
};
|
||||
|
||||
function minMaxTimePlugin(config) {
|
||||
if (config === void 0) { config = {}; }
|
||||
var state = {
|
||||
formatDate: createDateFormatter({}),
|
||||
tableDateFormat: config.tableDateFormat || "Y-m-d",
|
||||
defaults: {
|
||||
minTime: undefined,
|
||||
maxTime: undefined,
|
||||
},
|
||||
};
|
||||
function findDateTimeLimit(date) {
|
||||
if (config.table !== undefined) {
|
||||
return config.table[state.formatDate(date, state.tableDateFormat)];
|
||||
}
|
||||
return config.getTimeLimits && config.getTimeLimits(date);
|
||||
}
|
||||
return function (fp) {
|
||||
return {
|
||||
onReady: function () {
|
||||
state.formatDate = this.formatDate;
|
||||
state.defaults = {
|
||||
minTime: this.config.minTime && state.formatDate(this.config.minTime, "H:i"),
|
||||
maxTime: this.config.maxTime && state.formatDate(this.config.maxTime, "H:i"),
|
||||
};
|
||||
fp.loadedPlugins.push("minMaxTime");
|
||||
},
|
||||
onChange: function () {
|
||||
var latest = this.latestSelectedDateObj;
|
||||
var matchingTimeLimit = latest && findDateTimeLimit(latest);
|
||||
if (latest && matchingTimeLimit !== undefined) {
|
||||
this.set(matchingTimeLimit);
|
||||
fp.config.minTime.setFullYear(latest.getFullYear());
|
||||
fp.config.maxTime.setFullYear(latest.getFullYear());
|
||||
fp.config.minTime.setMonth(latest.getMonth());
|
||||
fp.config.maxTime.setMonth(latest.getMonth());
|
||||
fp.config.minTime.setDate(latest.getDate());
|
||||
fp.config.maxTime.setDate(latest.getDate());
|
||||
if (fp.config.minTime > fp.config.maxTime) {
|
||||
var minBound = calculateSecondsSinceMidnight(fp.config.minTime.getHours(), fp.config.minTime.getMinutes(), fp.config.minTime.getSeconds());
|
||||
var maxBound = calculateSecondsSinceMidnight(fp.config.maxTime.getHours(), fp.config.maxTime.getMinutes(), fp.config.maxTime.getSeconds());
|
||||
var currentTime = calculateSecondsSinceMidnight(latest.getHours(), latest.getMinutes(), latest.getSeconds());
|
||||
if (currentTime > maxBound && currentTime < minBound) {
|
||||
var result = parseSeconds(minBound);
|
||||
fp.setDate(new Date(latest.getTime()).setHours(result[0], result[1], result[2]), false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (compareDates(latest, fp.config.maxTime, false) > 0) {
|
||||
fp.setDate(new Date(latest.getTime()).setHours(fp.config.maxTime.getHours(), fp.config.maxTime.getMinutes(), fp.config.maxTime.getSeconds(), fp.config.maxTime.getMilliseconds()), false);
|
||||
}
|
||||
else if (compareDates(latest, fp.config.minTime, false) < 0) {
|
||||
fp.setDate(new Date(latest.getTime()).setHours(fp.config.minTime.getHours(), fp.config.minTime.getMinutes(), fp.config.minTime.getSeconds(), fp.config.minTime.getMilliseconds()), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
var newMinMax = state.defaults || {
|
||||
minTime: undefined,
|
||||
maxTime: undefined,
|
||||
};
|
||||
this.set(newMinMax);
|
||||
if (!latest)
|
||||
return;
|
||||
var _a = fp.config, minTime = _a.minTime, maxTime = _a.maxTime;
|
||||
if (minTime && compareTimes(latest, minTime) < 0) {
|
||||
fp.setDate(new Date(latest.getTime()).setHours(minTime.getHours(), minTime.getMinutes(), minTime.getSeconds(), minTime.getMilliseconds()), false);
|
||||
}
|
||||
else if (maxTime && compareTimes(latest, maxTime) > 0) {
|
||||
fp.setDate(new Date(latest.getTime()).setHours(maxTime.getHours(), maxTime.getMinutes(), maxTime.getSeconds(), maxTime.getMilliseconds()));
|
||||
}
|
||||
//
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
return minMaxTimePlugin;
|
||||
|
||||
})));
|
||||
883
src/ui/app/static/libs/flatpickr/themes/airbnb.css
Normal file
883
src/ui/app/static/libs/flatpickr/themes/airbnb.css
Normal file
|
|
@ -0,0 +1,883 @@
|
|||
.flatpickr-calendar {
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
-webkit-animation: none;
|
||||
animation: none;
|
||||
direction: ltr;
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
width: 307.875px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-ms-touch-action: manipulation;
|
||||
touch-action: manipulation;
|
||||
background: #fff;
|
||||
-webkit-box-shadow: 1px 0 0 #eee, -1px 0 0 #eee, 0 1px 0 #eee, 0 -1px 0 #eee, 0 3px 13px rgba(0,0,0,0.08);
|
||||
box-shadow: 1px 0 0 #eee, -1px 0 0 #eee, 0 1px 0 #eee, 0 -1px 0 #eee, 0 3px 13px rgba(0,0,0,0.08);
|
||||
}
|
||||
.flatpickr-calendar.open,
|
||||
.flatpickr-calendar.inline {
|
||||
opacity: 1;
|
||||
max-height: 640px;
|
||||
visibility: visible;
|
||||
}
|
||||
.flatpickr-calendar.open {
|
||||
display: inline-block;
|
||||
z-index: 99999;
|
||||
}
|
||||
.flatpickr-calendar.animate.open {
|
||||
-webkit-animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.flatpickr-calendar.inline {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.flatpickr-calendar.static {
|
||||
position: absolute;
|
||||
top: calc(100% + 2px);
|
||||
}
|
||||
.flatpickr-calendar.static.open {
|
||||
z-index: 999;
|
||||
display: block;
|
||||
}
|
||||
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7) {
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1) {
|
||||
-webkit-box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||
box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||
}
|
||||
.flatpickr-calendar .hasWeeks .dayContainer,
|
||||
.flatpickr-calendar .hasTime .dayContainer {
|
||||
border-bottom: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.flatpickr-calendar .hasWeeks .dayContainer {
|
||||
border-left: 0;
|
||||
}
|
||||
.flatpickr-calendar.hasTime .flatpickr-time {
|
||||
height: 40px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.flatpickr-calendar.noCalendar.hasTime .flatpickr-time {
|
||||
height: auto;
|
||||
}
|
||||
.flatpickr-calendar:before,
|
||||
.flatpickr-calendar:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
border: solid transparent;
|
||||
content: '';
|
||||
height: 0;
|
||||
width: 0;
|
||||
left: 22px;
|
||||
}
|
||||
.flatpickr-calendar.rightMost:before,
|
||||
.flatpickr-calendar.arrowRight:before,
|
||||
.flatpickr-calendar.rightMost:after,
|
||||
.flatpickr-calendar.arrowRight:after {
|
||||
left: auto;
|
||||
right: 22px;
|
||||
}
|
||||
.flatpickr-calendar.arrowCenter:before,
|
||||
.flatpickr-calendar.arrowCenter:after {
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
}
|
||||
.flatpickr-calendar:before {
|
||||
border-width: 5px;
|
||||
margin: 0 -5px;
|
||||
}
|
||||
.flatpickr-calendar:after {
|
||||
border-width: 4px;
|
||||
margin: 0 -4px;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:before,
|
||||
.flatpickr-calendar.arrowTop:after {
|
||||
bottom: 100%;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:before {
|
||||
border-bottom-color: #eee;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:after {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom:before,
|
||||
.flatpickr-calendar.arrowBottom:after {
|
||||
top: 100%;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom:before {
|
||||
border-top-color: #eee;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom:after {
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.flatpickr-calendar:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.flatpickr-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.flatpickr-months {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.flatpickr-months .flatpickr-month {
|
||||
background: transparent;
|
||||
color: #3c3f40;
|
||||
fill: #3c3f40;
|
||||
height: 34px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 34px;
|
||||
padding: 10px;
|
||||
z-index: 3;
|
||||
color: #3c3f40;
|
||||
fill: #3c3f40;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-disabled {
|
||||
display: none;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month i,
|
||||
.flatpickr-months .flatpickr-next-month i {
|
||||
position: relative;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
*/
|
||||
left: 0;
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
/*
|
||||
*/
|
||||
}
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
*/
|
||||
right: 0;
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
/*
|
||||
*/
|
||||
}
|
||||
/*
|
||||
/*rtl:begin:ignore*/
|
||||
/*
|
||||
/*rtl:end:ignore*/
|
||||
.flatpickr-months .flatpickr-prev-month:hover,
|
||||
.flatpickr-months .flatpickr-next-month:hover {
|
||||
color: #f64747;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month:hover svg,
|
||||
.flatpickr-months .flatpickr-next-month:hover svg {
|
||||
fill: #f64747;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month svg,
|
||||
.flatpickr-months .flatpickr-next-month svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month svg path,
|
||||
.flatpickr-months .flatpickr-next-month svg path {
|
||||
-webkit-transition: fill 0.1s;
|
||||
transition: fill 0.1s;
|
||||
fill: inherit;
|
||||
}
|
||||
.numInputWrapper {
|
||||
position: relative;
|
||||
height: auto;
|
||||
}
|
||||
.numInputWrapper input,
|
||||
.numInputWrapper span {
|
||||
display: inline-block;
|
||||
}
|
||||
.numInputWrapper input {
|
||||
width: 100%;
|
||||
}
|
||||
.numInputWrapper input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
.numInputWrapper input::-webkit-outer-spin-button,
|
||||
.numInputWrapper input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.numInputWrapper span {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
padding: 0 4px 0 2px;
|
||||
height: 50%;
|
||||
line-height: 50%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(64,72,72,0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.numInputWrapper span:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
.numInputWrapper span:active {
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
.numInputWrapper span:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
.numInputWrapper span.arrowUp {
|
||||
top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.numInputWrapper span.arrowUp:after {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid rgba(64,72,72,0.6);
|
||||
top: 26%;
|
||||
}
|
||||
.numInputWrapper span.arrowDown {
|
||||
top: 50%;
|
||||
}
|
||||
.numInputWrapper span.arrowDown:after {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid rgba(64,72,72,0.6);
|
||||
top: 40%;
|
||||
}
|
||||
.numInputWrapper span svg {
|
||||
width: inherit;
|
||||
height: auto;
|
||||
}
|
||||
.numInputWrapper span svg path {
|
||||
fill: rgba(60,63,64,0.5);
|
||||
}
|
||||
.numInputWrapper:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
.numInputWrapper:hover span {
|
||||
opacity: 1;
|
||||
}
|
||||
.flatpickr-current-month {
|
||||
font-size: 135%;
|
||||
line-height: inherit;
|
||||
font-weight: 300;
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
width: 75%;
|
||||
left: 12.5%;
|
||||
padding: 7.48px 0 0 0;
|
||||
line-height: 1;
|
||||
height: 34px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
.flatpickr-current-month span.cur-month {
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
color: inherit;
|
||||
display: inline-block;
|
||||
margin-left: 0.5ch;
|
||||
padding: 0;
|
||||
}
|
||||
.flatpickr-current-month span.cur-month:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
.flatpickr-current-month .numInputWrapper {
|
||||
width: 6ch;
|
||||
width: 7ch\0;
|
||||
display: inline-block;
|
||||
}
|
||||
.flatpickr-current-month .numInputWrapper span.arrowUp:after {
|
||||
border-bottom-color: #3c3f40;
|
||||
}
|
||||
.flatpickr-current-month .numInputWrapper span.arrowDown:after {
|
||||
border-top-color: #3c3f40;
|
||||
}
|
||||
.flatpickr-current-month input.cur-year {
|
||||
background: transparent;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
cursor: text;
|
||||
padding: 0 0 0 0.5ch;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: 300;
|
||||
line-height: inherit;
|
||||
height: auto;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
vertical-align: initial;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
.flatpickr-current-month input.cur-year:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.flatpickr-current-month input.cur-year[disabled],
|
||||
.flatpickr-current-month input.cur-year[disabled]:hover {
|
||||
font-size: 100%;
|
||||
color: rgba(60,63,64,0.5);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months {
|
||||
appearance: menulist;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: 300;
|
||||
height: auto;
|
||||
line-height: inherit;
|
||||
margin: -1px 0 0 0;
|
||||
outline: none;
|
||||
padding: 0 0 0 0.5ch;
|
||||
position: relative;
|
||||
vertical-align: initial;
|
||||
-webkit-box-sizing: border-box;
|
||||
-webkit-appearance: menulist;
|
||||
-moz-appearance: menulist;
|
||||
width: auto;
|
||||
}
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months:focus,
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months:active {
|
||||
outline: none;
|
||||
}
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month {
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
.flatpickr-weekdays {
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
}
|
||||
.flatpickr-weekdays .flatpickr-weekdaycontainer {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
span.flatpickr-weekday {
|
||||
cursor: default;
|
||||
font-size: 90%;
|
||||
background: transparent;
|
||||
color: rgba(0,0,0,0.54);
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
display: block;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
font-weight: bolder;
|
||||
}
|
||||
.dayContainer,
|
||||
.flatpickr-weeks {
|
||||
padding: 1px 0 0 0;
|
||||
}
|
||||
.flatpickr-days {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: start;
|
||||
-webkit-align-items: flex-start;
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
width: 307.875px;
|
||||
}
|
||||
.flatpickr-days:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.dayContainer {
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
text-align: left;
|
||||
width: 307.875px;
|
||||
min-width: 307.875px;
|
||||
max-width: 307.875px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
-ms-flex-pack: justify;
|
||||
-webkit-justify-content: space-around;
|
||||
justify-content: space-around;
|
||||
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
opacity: 1;
|
||||
}
|
||||
.dayContainer + .dayContainer {
|
||||
-webkit-box-shadow: -1px 0 0 #eee;
|
||||
box-shadow: -1px 0 0 #eee;
|
||||
}
|
||||
.flatpickr-day {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 150px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #404848;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
width: 14.2857143%;
|
||||
-webkit-flex-basis: 14.2857143%;
|
||||
-ms-flex-preferred-size: 14.2857143%;
|
||||
flex-basis: 14.2857143%;
|
||||
max-width: 39px;
|
||||
height: 39px;
|
||||
line-height: 39px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.flatpickr-day.inRange,
|
||||
.flatpickr-day.prevMonthDay.inRange,
|
||||
.flatpickr-day.nextMonthDay.inRange,
|
||||
.flatpickr-day.today.inRange,
|
||||
.flatpickr-day.prevMonthDay.today.inRange,
|
||||
.flatpickr-day.nextMonthDay.today.inRange,
|
||||
.flatpickr-day:hover,
|
||||
.flatpickr-day.prevMonthDay:hover,
|
||||
.flatpickr-day.nextMonthDay:hover,
|
||||
.flatpickr-day:focus,
|
||||
.flatpickr-day.prevMonthDay:focus,
|
||||
.flatpickr-day.nextMonthDay:focus {
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
background: #e9e9e9;
|
||||
border-color: #e9e9e9;
|
||||
}
|
||||
.flatpickr-day.today {
|
||||
border-color: #f64747;
|
||||
}
|
||||
.flatpickr-day.today:hover,
|
||||
.flatpickr-day.today:focus {
|
||||
border-color: #f64747;
|
||||
background: #f64747;
|
||||
color: #fff;
|
||||
}
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.endRange,
|
||||
.flatpickr-day.selected.inRange,
|
||||
.flatpickr-day.startRange.inRange,
|
||||
.flatpickr-day.endRange.inRange,
|
||||
.flatpickr-day.selected:focus,
|
||||
.flatpickr-day.startRange:focus,
|
||||
.flatpickr-day.endRange:focus,
|
||||
.flatpickr-day.selected:hover,
|
||||
.flatpickr-day.startRange:hover,
|
||||
.flatpickr-day.endRange:hover,
|
||||
.flatpickr-day.selected.prevMonthDay,
|
||||
.flatpickr-day.startRange.prevMonthDay,
|
||||
.flatpickr-day.endRange.prevMonthDay,
|
||||
.flatpickr-day.selected.nextMonthDay,
|
||||
.flatpickr-day.startRange.nextMonthDay,
|
||||
.flatpickr-day.endRange.nextMonthDay {
|
||||
background: #4f99ff;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
border-color: #4f99ff;
|
||||
}
|
||||
.flatpickr-day.selected.startRange,
|
||||
.flatpickr-day.startRange.startRange,
|
||||
.flatpickr-day.endRange.startRange {
|
||||
border-radius: 50px 0 0 50px;
|
||||
}
|
||||
.flatpickr-day.selected.endRange,
|
||||
.flatpickr-day.startRange.endRange,
|
||||
.flatpickr-day.endRange.endRange {
|
||||
border-radius: 0 50px 50px 0;
|
||||
}
|
||||
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)),
|
||||
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)),
|
||||
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) {
|
||||
-webkit-box-shadow: -10px 0 0 #4f99ff;
|
||||
box-shadow: -10px 0 0 #4f99ff;
|
||||
}
|
||||
.flatpickr-day.selected.startRange.endRange,
|
||||
.flatpickr-day.startRange.startRange.endRange,
|
||||
.flatpickr-day.endRange.startRange.endRange {
|
||||
border-radius: 50px;
|
||||
}
|
||||
.flatpickr-day.inRange {
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: -5px 0 0 #e9e9e9, 5px 0 0 #e9e9e9;
|
||||
box-shadow: -5px 0 0 #e9e9e9, 5px 0 0 #e9e9e9;
|
||||
}
|
||||
.flatpickr-day.flatpickr-disabled,
|
||||
.flatpickr-day.flatpickr-disabled:hover,
|
||||
.flatpickr-day.prevMonthDay,
|
||||
.flatpickr-day.nextMonthDay,
|
||||
.flatpickr-day.notAllowed,
|
||||
.flatpickr-day.notAllowed.prevMonthDay,
|
||||
.flatpickr-day.notAllowed.nextMonthDay {
|
||||
color: rgba(64,72,72,0.3);
|
||||
background: transparent;
|
||||
border-color: #e9e9e9;
|
||||
cursor: default;
|
||||
}
|
||||
.flatpickr-day.flatpickr-disabled,
|
||||
.flatpickr-day.flatpickr-disabled:hover {
|
||||
cursor: not-allowed;
|
||||
color: rgba(64,72,72,0.1);
|
||||
}
|
||||
.flatpickr-day.week.selected {
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: -5px 0 0 #4f99ff, 5px 0 0 #4f99ff;
|
||||
box-shadow: -5px 0 0 #4f99ff, 5px 0 0 #4f99ff;
|
||||
}
|
||||
.flatpickr-day.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
.rangeMode .flatpickr-day {
|
||||
margin-top: 1px;
|
||||
}
|
||||
.flatpickr-weekwrapper {
|
||||
float: left;
|
||||
}
|
||||
.flatpickr-weekwrapper .flatpickr-weeks {
|
||||
padding: 0 12px;
|
||||
-webkit-box-shadow: 1px 0 0 #eee;
|
||||
box-shadow: 1px 0 0 #eee;
|
||||
}
|
||||
.flatpickr-weekwrapper .flatpickr-weekday {
|
||||
float: none;
|
||||
width: 100%;
|
||||
line-height: 28px;
|
||||
}
|
||||
.flatpickr-weekwrapper span.flatpickr-day,
|
||||
.flatpickr-weekwrapper span.flatpickr-day:hover {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
color: rgba(64,72,72,0.3);
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
border: none;
|
||||
}
|
||||
.flatpickr-innerContainer {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.flatpickr-rContainer {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.flatpickr-time {
|
||||
text-align: center;
|
||||
outline: 0;
|
||||
display: block;
|
||||
height: 0;
|
||||
line-height: 40px;
|
||||
max-height: 40px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.flatpickr-time:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.flatpickr-time .numInputWrapper {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
width: 40%;
|
||||
height: 40px;
|
||||
float: left;
|
||||
}
|
||||
.flatpickr-time .numInputWrapper span.arrowUp:after {
|
||||
border-bottom-color: #404848;
|
||||
}
|
||||
.flatpickr-time .numInputWrapper span.arrowDown:after {
|
||||
border-top-color: #404848;
|
||||
}
|
||||
.flatpickr-time.hasSeconds .numInputWrapper {
|
||||
width: 26%;
|
||||
}
|
||||
.flatpickr-time.time24hr .numInputWrapper {
|
||||
width: 49%;
|
||||
}
|
||||
.flatpickr-time input {
|
||||
background: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
color: #404848;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
.flatpickr-time input.flatpickr-hour {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flatpickr-time input.flatpickr-minute,
|
||||
.flatpickr-time input.flatpickr-second {
|
||||
font-weight: 400;
|
||||
}
|
||||
.flatpickr-time input:focus {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
.flatpickr-time .flatpickr-time-separator,
|
||||
.flatpickr-time .flatpickr-am-pm {
|
||||
height: inherit;
|
||||
float: left;
|
||||
line-height: inherit;
|
||||
color: #404848;
|
||||
font-weight: bold;
|
||||
width: 2%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-align-self: center;
|
||||
-ms-flex-item-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
.flatpickr-time .flatpickr-am-pm {
|
||||
outline: 0;
|
||||
width: 18%;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
.flatpickr-time input:hover,
|
||||
.flatpickr-time .flatpickr-am-pm:hover,
|
||||
.flatpickr-time input:focus,
|
||||
.flatpickr-time .flatpickr-am-pm:focus {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.flatpickr-input[readonly] {
|
||||
cursor: pointer;
|
||||
}
|
||||
@-webkit-keyframes fpFadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, -20px, 0);
|
||||
transform: translate3d(0, -20px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes fpFadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, -20px, 0);
|
||||
transform: translate3d(0, -20px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
.flatpickr-calendar {
|
||||
width: 307.875px;
|
||||
}
|
||||
.dayContainer {
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
span.flatpickr-day,
|
||||
span.flatpickr-day.prevMonthDay,
|
||||
span.flatpickr-day.nextMonthDay {
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid #e9e9e9;
|
||||
max-width: none;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
span.flatpickr-day:nth-child(n+8),
|
||||
span.flatpickr-day.prevMonthDay:nth-child(n+8),
|
||||
span.flatpickr-day.nextMonthDay:nth-child(n+8) {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
span.flatpickr-day:nth-child(7n-6),
|
||||
span.flatpickr-day.prevMonthDay:nth-child(7n-6),
|
||||
span.flatpickr-day.nextMonthDay:nth-child(7n-6) {
|
||||
border-left: 0;
|
||||
}
|
||||
span.flatpickr-day:nth-child(n+36),
|
||||
span.flatpickr-day.prevMonthDay:nth-child(n+36),
|
||||
span.flatpickr-day.nextMonthDay:nth-child(n+36) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
span.flatpickr-day:nth-child(-n+7),
|
||||
span.flatpickr-day.prevMonthDay:nth-child(-n+7),
|
||||
span.flatpickr-day.nextMonthDay:nth-child(-n+7) {
|
||||
margin-top: 0;
|
||||
}
|
||||
span.flatpickr-day.today:not(.selected),
|
||||
span.flatpickr-day.prevMonthDay.today:not(.selected),
|
||||
span.flatpickr-day.nextMonthDay.today:not(.selected) {
|
||||
border-color: #e9e9e9;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #f64747;
|
||||
}
|
||||
span.flatpickr-day.today:not(.selected):hover,
|
||||
span.flatpickr-day.prevMonthDay.today:not(.selected):hover,
|
||||
span.flatpickr-day.nextMonthDay.today:not(.selected):hover {
|
||||
border: 1px solid #f64747;
|
||||
}
|
||||
span.flatpickr-day.startRange,
|
||||
span.flatpickr-day.prevMonthDay.startRange,
|
||||
span.flatpickr-day.nextMonthDay.startRange,
|
||||
span.flatpickr-day.endRange,
|
||||
span.flatpickr-day.prevMonthDay.endRange,
|
||||
span.flatpickr-day.nextMonthDay.endRange {
|
||||
border-color: #4f99ff;
|
||||
}
|
||||
span.flatpickr-day.today,
|
||||
span.flatpickr-day.prevMonthDay.today,
|
||||
span.flatpickr-day.nextMonthDay.today,
|
||||
span.flatpickr-day.selected,
|
||||
span.flatpickr-day.prevMonthDay.selected,
|
||||
span.flatpickr-day.nextMonthDay.selected {
|
||||
z-index: 2;
|
||||
}
|
||||
.rangeMode .flatpickr-day {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.flatpickr-weekwrapper .flatpickr-weeks {
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.flatpickr-weekwrapper span.flatpickr-day {
|
||||
border: 0;
|
||||
margin: -1px 0 0 -1px;
|
||||
}
|
||||
.hasWeeks .flatpickr-days {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width:0\0) and (min-resolution: +72dpi) {
|
||||
span.flatpickr-day {
|
||||
display: block;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
185
src/ui/app/templates/bans.html
Normal file
185
src/ui/app/templates/bans.html
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="bans_number" value="{{ bans|length }}" />
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="bans" class="table position-relative 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 date and time when the Ban was created">Date</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The banned IP address">IP address</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The reason why the Report was created">Reason</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The end date of the Ban">End date</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The time left until the Ban expires">Time left</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The actions that can be performed on the Ban">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ban in bans %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="ban-start-date">{{ ban["start_date"] }}</td>
|
||||
<td>{{ ban["ip"] }}</td>
|
||||
<td>{{ ban["reason"] }}</td>
|
||||
<td class="ban-end-date">{{ ban["end_date"] }}</td>
|
||||
<td>{{ ban["remain"] }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Unban {{ ban['ip'] }}">
|
||||
<button type="button"
|
||||
data-ip="{{ ban['ip'] }}"
|
||||
data-time-left="{{ ban['remain'] }}"
|
||||
class="btn btn-outline-danger btn-sm me-1 unban-ip">
|
||||
<i class="bx bxs-buoy bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal modal-xl fade"
|
||||
id="modal-ban-ips"
|
||||
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">Add Ban(s)</h5>
|
||||
<button id="add-ban"
|
||||
type="button"
|
||||
class="btn btn-text-bw-green rounded-pill p-0 ms-4 me-2">
|
||||
<i class="bx bx-plus-circle"></i> INSERT
|
||||
</button>
|
||||
<button id="clear-bans"
|
||||
type="button"
|
||||
class="btn btn-text-danger rounded-pill p-0 ms-4">
|
||||
<i class="bx bx-trash"></i> CLEAR
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="bans-form" action="{{ url_for("bans") }}/ban" method="POST">
|
||||
<div class="modal-body justify-content-center">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<ul id="bans-container" class="list-group rounded-top w-100">
|
||||
<li id="bans-header" class="list-group-item bg-secondary text-white">
|
||||
<div class="row">
|
||||
<div class="col-3 text-center fw-bold">IP Address</div>
|
||||
<div class="col-5 border-start text-center fw-bold">End Date</div>
|
||||
<div class="col-3 border-start text-center fw-bold">Reason</div>
|
||||
<div class="col-1 border-start text-center fw-bold">Delete</div>
|
||||
</div>
|
||||
</li>
|
||||
<li id="ban-1" class="list-group-item rounded-0">
|
||||
<div class="row align-items-center d-flex">
|
||||
<div class="col-3">
|
||||
<input type="text"
|
||||
name="ip"
|
||||
class="form-control"
|
||||
placeholder="127.0.0.1"
|
||||
required />
|
||||
</div>
|
||||
<div class="col-5 border-start">
|
||||
<input type="flapickr-datetime"
|
||||
name="datetime"
|
||||
class="form-control"
|
||||
required />
|
||||
</div>
|
||||
<div class="col-3 border-start">
|
||||
<input type="text" name="reason" class="form-control" value="ui" required />
|
||||
</div>
|
||||
<div class="col-1 border-start align-items-center d-flex"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right"
|
||||
data-bs-original-title="Can't delete the original Ban">
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm me-1 delete-ban disabled">
|
||||
<i class="bx bx-trash bx-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Ban</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-lg fade"
|
||||
id="modal-unban-ips"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
role="dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirm Unban</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for("bans") }}/unban" method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" id="selected-ips-input-unban" name="ips" value="" />
|
||||
<div class="alert alert-danger text-center" role="alert">
|
||||
Are you sure you want to unban the selected IP addresses?
|
||||
</div>
|
||||
<ul id="selected-ips-unban" class="list-group w-100 mb-3">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="submit" class="btn btn-outline-danger me-2">Unban</button>
|
||||
<button type="reset"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
@ -25,12 +25,21 @@
|
|||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='fonts/boxicons.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache") %}
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache", "reports", "bans", "jobs") %}
|
||||
<!-- Datatables -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/datatables/datatables.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% endif %}
|
||||
{% if current_endpoint in ("bans") %}
|
||||
<!-- Flatpickr -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/flatpickr/flatpickr.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/flatpickr/themes/airbnb.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% endif %}
|
||||
<!-- Core CSS -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/core.css') }}"
|
||||
|
|
@ -89,11 +98,17 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='libs/purify/purify.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache") %}
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache", "reports", "bans", "jobs") %}
|
||||
<script src="{{ url_for('static', filename='libs/datatables/datatables.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% if current_endpoint not in ("services", "configs", "cache") and ("services" in request.path or "configs" in request.path or "cache" in request.path) %}
|
||||
{% if current_endpoint in ("bans") %}
|
||||
<script src="{{ url_for('static', filename='libs/flatpickr/flatpickr.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='libs/flatpickr/plugins/minMaxTimePlugin.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% if current_endpoint not in ("services", "configs", "cache") and ("services" in request.path or "configs" in request.path or "cache" in request.path or current_endpoint == "logs") %}
|
||||
<script src="{{ url_for('static', filename='libs/ace/src-min/ace.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
|
|
@ -143,6 +158,18 @@
|
|||
{% elif current_endpoint != "cache" and "cache" in request.path %}
|
||||
<script src="{{ url_for('static', filename='js/pages/cache_view.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "reports" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/reports.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "bans" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/bans.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "jobs" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/jobs.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "logs" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/logs.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
<script async defer src="{{ url_for('static', filename='js/buttons.js') }}"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -7,30 +7,32 @@
|
|||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="cache" class="table w-100">
|
||||
<table id="cache" class="table position-relative w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The user defined Cache's file name">File name</th>
|
||||
data-bs-original-title="The Cache file's name">File name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Cache's job name">Job name</th>
|
||||
data-bs-original-title="The Cache file's job name">Job name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Cache's job's Plugin">Plugin</th>
|
||||
data-bs-original-title="The Cache file's job's Plugin">Plugin</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Service associated with the Cache">Service</th>
|
||||
data-bs-original-title="The Service associated with the Cache file">Service</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The date and time when the Cache was last updated">Last Update</th>
|
||||
data-bs-original-title="The date and time when the Cache file was last updated">
|
||||
Last Update
|
||||
</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Cache's checksum">Checksum</th>
|
||||
data-bs-original-title="The Cache file's checksum">Checksum</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The actions that can be performed on the Instance">Actions</th>
|
||||
data-bs-original-title="The actions that can be performed on the Cache file">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -38,7 +40,7 @@
|
|||
{% set service_id = cache['service_id'] if cache['service_id'] else 'global' %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') }}"
|
||||
<a href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') if cache['file_name'].startswith('folder:') else cache['file_name'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="View {{ cache['file_name'] }}"><i class="bx bx-show bx-xs"></i> {{ cache['file_name'] }}</a>
|
||||
|
|
@ -55,23 +57,23 @@
|
|||
<span class="badge rounded-pill bg-label-secondary">global</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ cache["last_update"] if cache["last_update"] == "Never" else cache["last_update"].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}
|
||||
<td class="cache-last-update-date">
|
||||
{{ cache["last_update"] if cache["last_update"] == "Never" else cache["last_update"].astimezone().isoformat() }}
|
||||
</td>
|
||||
<td>{{ cache["checksum"] }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-sm me-1"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') }}"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') if cache['file_name'].startswith('folder:') else cache['file_name'] }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="View {{ cache['file_name'] }}">
|
||||
<i class="bx bx-show bx-xs"></i>
|
||||
</a>
|
||||
<a role="button"
|
||||
class="btn btn-outline-secondary btn-sm me-1"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') }}?download=true"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ cache['plugin_id'] }}/{{ cache['job_name'] }}/{{ cache['file_name'].replace('/', '_') if cache['file_name'].startswith('folder:') else cache['file_name'] }}?download=true"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Download {{ cache['file_name'] }}"
|
||||
|
|
@ -84,6 +86,9 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</table>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card position-relative p-4 min-vh-70">
|
||||
<div class="position-absolute top-0 end-0 m-2" style="z-index: 1000">
|
||||
<div class="card p-1 me-2">
|
||||
<div class="position-absolute bottom-0 end-0 m-4" style="z-index: 1000">
|
||||
<div class="card p-1">
|
||||
<a role="button"
|
||||
class="btn btn-sm btn-outline-secondary "
|
||||
href="{{ url_for("cache") }}/{{ '/'.join(request.path.split('/')[2:]) }}?download=true"
|
||||
href="{{ url_for("cache") }}/{{ '/'.join(request.path.split('/')[2:] ) }}?download=true"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Download file"
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
{% if not config_template and config_method and config_method != "ui" %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ config_method }}"{% endif %}>
|
||||
<div id="config-value"
|
||||
data-language="{% if type and type.startswith(('CRS', 'MODSEC')) %}ModSecurity{% else %}NGINX{% endif %}"
|
||||
data-method="{{ config_method }}"
|
||||
data-method="{{ config_method or 'ui' }}"
|
||||
data-template="{{ config_template }}"
|
||||
class="ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ config_value }}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
data-bs-original-title="The Custom config's checksum">Checksum</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The actions that can be performed on the Instance">Actions</th>
|
||||
data-bs-original-title="The actions that can be performed on the Custom config">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="instances" class="table w-100">
|
||||
<table id="instances" class="table position-relative w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
|
@ -87,8 +87,8 @@
|
|||
<i class="bx bx-microchip"></i> Static
|
||||
{% endif %}
|
||||
</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 class="instance-creation-date">{{ instance.creation_date.astimezone().isoformat() }}</td>
|
||||
<td class="instance-last-seen-date">{{ instance.last_seen.astimezone().isoformat() }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<button type="button"
|
||||
|
|
@ -131,6 +131,9 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</table>
|
||||
</div>
|
||||
<div id="feedback-toast"
|
||||
|
|
|
|||
145
src/ui/app/templates/jobs.html
Normal file
145
src/ui/app/templates/jobs.html
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="job_number" value="{{ jobs|length }}" />
|
||||
<table id="jobs" class="table position-relative w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Job's name">Name</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Job's Plugin">Plugin</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Job's interval">Interval</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Does the Job needs a reload?">Reload</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Does the last Job's execution was successful?">Last run</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The Job's information">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job, job_data in jobs.items() %}
|
||||
<tr>
|
||||
<td>{{ job }}</td>
|
||||
<td>{{ job_data["plugin_id"] }}</td>
|
||||
<td>
|
||||
<i class="bx {% if job_data['every'] == 'once' %}bx-check-square{% elif job_data['every'] == 'day' %}bx-calendar-event{% elif job_data['every'] == 'week' %}bx-calendar-week{% else %}bxs-hourglass{% endif %}"></i> {{ job_data["every"] }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<i class="bx bx-sm bx-{% if job_data['reload'] %}check-circle text-success{% else %}x-circle text-danger{% endif %}"></i>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if job_data['history'] %}
|
||||
<i class="bx bx-sm bx-{% if job_data['history'][0]['success'] %}check-circle text-success{% else %}x-circle text-danger{% endif %}"></i>
|
||||
{% else %}
|
||||
No history
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
{% if job_data['history'] %}
|
||||
<div id="job-{{ job }}-{{ job_data['plugin_id'] }}-history"
|
||||
class="visually-hidden mb-3">
|
||||
<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white rounded-0 rounded-top"
|
||||
style="flex: 1 1 0">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Start date</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white rounded-0 rounded-top"
|
||||
style="flex: 1 1 0">
|
||||
<div class="fw-bold">End date</div>
|
||||
</li>
|
||||
<li class="list-group-item align-items-center text-center bg-secondary text-white rounded-0 rounded-top"
|
||||
style="flex: 1 1 0">
|
||||
<div class="fw-bold">Success</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% for history in job_data['history'] %}
|
||||
<ul class="list-group list-group-horizontal d-flex w-100">
|
||||
<li class="list-group-item align-items-center history-start-date rounded-0{% if loop.index == job_data['history']|length %} rounded-bottom{% endif %}"
|
||||
style="flex: 1 1 0">{{ history['start_date'] }}</li>
|
||||
<li class="list-group-item align-items-center history-end-date rounded-0{% if loop.index == job_data['history']|length %} rounded-bottom{% endif %}"
|
||||
style="flex: 1 1 0">{{ history['end_date'] }}</li>
|
||||
<li class="list-group-item align-items-center text-center rounded-0{% if loop.index == job_data['history']|length %} rounded-bottom{% endif %}" style="flex: 1 1 0">
|
||||
<i class="bx bx-{% if history['success'] %}check-circle text-success{% else %}x-circle text-danger{% endif %}"></i>
|
||||
</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if not job_data['history'] %}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="No history"{% endif %}>
|
||||
<button type="button"
|
||||
data-job="{{ job }}"
|
||||
data-plugin="{{ job_data['plugin_id'] }}"
|
||||
class="btn btn-outline-primary btn-sm me-1 show-history{% if not job_data['history'] %} disabled{% endif %}">
|
||||
<i class="bx bx-history bx-xs"></i> History
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown btn-group"
|
||||
{% if not job_data['cache'] %}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="No cache"{% endif %}>
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm dropdown-toggle{% if not job_data['cache'] %} disabled{% endif %}"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="bx bx-data"></i>
|
||||
<span class="d-none d-md-inline"> Cache</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu nav-pills max-vh-60 overflow-auto pt-0"
|
||||
role="tablist">
|
||||
{% for cache in job_data['cache'] %}
|
||||
{% set service_id = cache['service_id'] if cache['service_id'] else 'global' %}
|
||||
<li class="nav-item">
|
||||
<a role="button"
|
||||
class="dropdown-item"
|
||||
href="{{ url_for("cache") }}/{{ service_id }}/{{ job_data['plugin_id'] }}/{{ job }}/{{ cache['file_name'].replace('/', '_') if cache['file_name'].startswith('folder:') else cache['file_name'] }}?download=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<i class="tf-icons bx bx-download bx-xs me-1"></i>{{ cache["file_name"] }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal modal-lg fade"
|
||||
id="modal-job-history"
|
||||
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">Job History</h5>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
72
src/ui/app/templates/logs.html
Normal file
72
src/ui/app/templates/logs.html
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card p-1 mb-4 sticky-card">
|
||||
<div class="d-flex flex-wrap justify-content-around align-items-center">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="dropdown btn-group"
|
||||
{% if not files %}data-bs-toggle="tooltip" data-bs-placement="top" title="No log files available"{% endif %}>
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle{% if not files %} disabled{% endif %}"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="bx bx-file"></i>
|
||||
<span class="d-none d-md-inline"> Select File</span>
|
||||
</button>
|
||||
<ul id="types-dropdown-menu"
|
||||
class="dropdown-menu nav-pills max-vh-60 overflow-auto pt-0"
|
||||
role="tablist">
|
||||
{% for file in files %}
|
||||
<li class="nav-item">
|
||||
<a role="button"
|
||||
href="{{ url_for('logs', file=file) }}"
|
||||
class="dropdown-item{% if current_file and current_file == file %} active{% endif %}">
|
||||
{{ file }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if page_num > 1 %}
|
||||
<div class="dropdown btn-group">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle ms-2"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="bx bx-file-find"></i>
|
||||
<span class="d-none d-md-inline"> Select Page</span>
|
||||
</button>
|
||||
<ul id="pages-dropdown-menu"
|
||||
class="dropdown-menu nav-pills max-vh-60 overflow-auto pt-0"
|
||||
role="tablist">
|
||||
{% for page in range(page_num, 0, -1) %}
|
||||
<li class="nav-item">
|
||||
<a role="button"
|
||||
href="{% if loop.index == 1 %}{{ url_for('logs', file=current_file) }}{% else %}{{ url_for('logs', file=current_file, page=page) }}{% endif %}"
|
||||
class="dropdown-item{% if current_page == page %} active{% endif %}">
|
||||
Page {{ page }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div {% if not request.is_secure %}data-bs-toggle="tooltip" data-bs-placement="top" title="The copy feature is only available over HTTPS"{% endif %}>
|
||||
<button id="copy-logs"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary {% if not request.is_secure %}disabled{% endif %}">
|
||||
<i class="bx bx-copy-alt"></i>
|
||||
<span class="d-none d-md-inline"> Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-4 min-vh-70">
|
||||
<div id="raw-logs"
|
||||
class="ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ logs }}</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"configs": {"url": url_for('configs'), "icon": "bx-wrench"},
|
||||
"plugins": {"url": url_for('plugins'), "icon": "bx-plug"},
|
||||
"cache": {"url": url_for('cache'), "icon": "bx-data"},
|
||||
"reports": {"url": url_for('reports'), "icon": "bx-bar-chart-alt-2"},
|
||||
"reports": {"url": url_for('reports'), "icon": "bxs-flag-checkered"},
|
||||
"bans": {"url": url_for('bans'), "icon": "bx-block"},
|
||||
"jobs": {"url": url_for('jobs'), "icon": "bx-time-five"},
|
||||
"logs": {"url": url_for('logs'), "icon": "bx-file-find"},
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
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">
|
||||
{% if request.is_secure %}
|
||||
<div class="card p-1 me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary copy-settings">
|
||||
<i class="bx bx-copy-alt bx-xs"></i>
|
||||
<span class="d-none d-md-inline"> Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card p-1 me-2" {% if not request.is_secure %}data-bs-toggle="tooltip" data-bs-placement="top" title="The copy feature is only available over HTTPS"{% endif %}>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary copy-settings{% if not request.is_secure %} disabled{% endif %}">
|
||||
<i class="bx bx-copy-alt bx-xs"></i>
|
||||
<span class="d-none d-md-inline"> Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card p-1"
|
||||
{% if service_method == "autoconf" %} data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
|
||||
<!-- Save button container -->
|
||||
|
|
|
|||
59
src/ui/app/templates/reports.html
Normal file
59
src/ui/app/templates/reports.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{% extends "dashboard.html" %}
|
||||
{% block content %}
|
||||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="reports_number" value="{{ reports|length }}" />
|
||||
<table id="reports" class="table position-relative w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The date and time when the Report was created">Date</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The reported IP address">IP Address</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The country of the reported IP address">Country</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The method used by the attacker">Method</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The URL that was targeted by the attacker">URL</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The HTTP status code returned by BunkerWeb">Status Code</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The User-Agent of the attacker">User-Agent</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="The reason why the Report was created">Reason</th>
|
||||
<th data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-original-title="Additional data about the Report">Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for report in reports %}
|
||||
<tr>
|
||||
<td class="report-date">{{ report["date"] }}</td>
|
||||
<td>{{ report["ip"] }}</td>
|
||||
<td>{{ report["country"] }}</td>
|
||||
<td>{{ report["method"] }}</td>
|
||||
<td>{{ report["url"] }}</td>
|
||||
<td>{{ report["status"] }}</td>
|
||||
<td>{{ report["user_agent"] }}</td>
|
||||
<td>{{ report["reason"] }}</td>
|
||||
<td>{{ report["data"] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</table>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<table id="services" class="table w-100">
|
||||
<table id="services" class="table position-relative w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
|
@ -59,8 +59,8 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td id="method-{{ service['id'].replace('.', '-') }}">{{ service["method"] }}</td>
|
||||
<td>{{ service['creation_date'].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td>{{ service['last_update'].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") }}</td>
|
||||
<td class="service-creation-date">{{ service['creation_date'].astimezone().isoformat() }}</td>
|
||||
<td class="service-last-update-date">{{ service['last_update'].astimezone().isoformat() }}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a role="button"
|
||||
|
|
@ -112,6 +112,9 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<span class="position-absolute bottom-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</table>
|
||||
</div>
|
||||
<div id="feedback-toast"
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ def inject_variables():
|
|||
flash("The last changes have been applied successfully.", "success")
|
||||
DATA["CONFIG_CHANGED"] = False
|
||||
|
||||
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, with_drafts=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
|
||||
# check that is value is in tuple
|
||||
return dict(
|
||||
|
|
|
|||
Loading…
Reference in a new issue