Merge pull request #373 from TheophileDiot/dev

Advancements on the UI + some fixes
This commit is contained in:
Théophile Diot 2022-11-29 15:17:16 +01:00 committed by GitHub
commit 6bbbe70eea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1661 additions and 960 deletions

View File

@ -23,21 +23,7 @@ services:
- bunkerweb.LIMIT_REQ_URL_1=/core/install.php
- bunkerweb.LIMIT_REQ_RATE_1=5r/s
mydb:
image: mariadb
networks:
- bw-services
volumes:
- db-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=drupaldb
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password
deploy:
placement:
constraints:
- "node.role==worker"
# For the database, you can refer to the swarm example including a database
networks:
bw-services:

View File

@ -24,7 +24,7 @@ services:
- bunkerweb.LIMIT_REQ_URL_2=/installation/index.php
- bunkerweb.LIMIT_REQ_RATE_2=8r/s
# For the database, you can refer to the example of the autoconf including a database
# For the database, you can refer to the autoconf integration example including a database
networks:
bw-services:

View File

@ -26,21 +26,7 @@ services:
- bunkerweb.LIMIT_REQ_URL_2=/installation/index.php
- bunkerweb.LIMIT_REQ_RATE_2=8r/s
mydb:
image: mariadb
volumes:
- db_data:/var/lib/mysql
networks:
- bw-services
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=joomla_db
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password (must match JOOMLA_DB_PASSWORD)
deploy:
placement:
constraints:
- "node.role==worker"
# For the database, you can refer to the swarm integration example including a database
networks:
bw-services:

View File

@ -43,7 +43,7 @@ services:
volumes:
- ./elasticsearch-data:/bitnami/elasticsearch/data
# For the database, you can refer to the example of the autoconf including a database
# For the database, you can refer to the autoconf integration example including a database
networks:
bw-services:

View File

@ -40,21 +40,7 @@ services:
constraints:
- "node.role==worker"
mydb:
image: mariadb:10.2
networks:
- bw-services
volumes:
- db_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=magentodb
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password (must match MAGENTO_DATABASE_PASSWORD)
deploy:
placement:
constraints:
- "node.role==worker"
# For the database, you can refer to the swarm integration example including a database
networks:
bw-services:

View File

@ -63,7 +63,7 @@ services:
- bunkerweb.LIMIT_REQ_URL_3=^/static/
- bunkerweb.LIMIT_REQ_RATE_3=10r/s
# For the postgres database, you can refer to the example of the autoconf including a postgres database
# For the postgres database, you can refer to the autoconf integration example including a postgres database
networks:
bw-services:

View File

@ -27,7 +27,7 @@ services:
- bunkerweb.REVERSE_PROXY_URL=/
- bunkerweb.REVERSE_PROXY_HOST=https://mymoodle:8443
# For the database, you can refer to the example of the autoconf including a database
# For the database, you can refer to the autoconf integration example including a database
# In this example, you will need to add the following lines to the mydb service:
# - MARIADB_CHARACTER_SET=utf8mb4
# - MARIADB_COLLATE=utf8mb4_unicode_ci

View File

@ -29,23 +29,10 @@ services:
- bunkerweb.REVERSE_PROXY_URL=/
- bunkerweb.REVERSE_PROXY_HOST=https://mymoodle:8443
mydb:
image: mariadb:10.5
volumes:
- db_data:/var/lib/mysql
networks:
- bw-services
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=moodle
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password (must match MOODLE_DATABASE_PASSWORD)
- MARIADB_CHARACTER_SET=utf8mb4
- MARIADB_COLLATE=utf8mb4_unicode_ci
deploy:
placement:
constraints:
- "node.role==worker"
# For the database, you can refer to the swarm integration example including a database
# In this example, you will need to add the following lines to the mydb service:
# - MARIADB_CHARACTER_SET=utf8mb4
# - MARIADB_COLLATE=utf8mb4_unicode_ci
networks:
bw-services:

View File

@ -56,7 +56,7 @@ services:
bunkerweb.CUSTOM_CONF_MODSEC_nextcloud=
SecRule REQUEST_FILENAME "@rx ^/remote.php/dav/files/" "id:1000,ctl:ruleRemoveByTag=attack-protocol,ctl:ruleRemoveByTag=attack-generic,nolog"
# For the database, you can refer to the example of the autoconf including a database
# For the database, you can refer to the autoconf integration example including a database
networks:
bw-services:

View File

@ -37,7 +37,7 @@ services:
- bunkerweb.LIMIT_REQ_URL_3=/core/preview
- bunkerweb.LIMIT_REQ_RATE_3=5r/s
# For the database, you can refer to the example of the autoconf in swarm mode including a database
# For the database, you can refer to the swarm integration example including a database
networks:
bw-services:

View File

@ -2,10 +2,24 @@ version: "3"
services:
# you will need to add a user by hand
# example : docker-compose exec mypassbolt su -m -c "bin/cake passbolt register_user -u your@email.com -f yourname -l surname -r admin" -s /bin/sh www-data
# example : docker-compose exec mypassbolt su -m -c "/usr/share/php/passbolt/bin/cake passbolt register_user -u <your@email.com> -f <yourname> -l <surname> -r admin" -s /bin/sh www-data
# more info at https://github.com/passbolt/passbolt_docker
mypassbolt:
image: passbolt/passbolt
image: passbolt/passbolt:3.8.1-1-ce
networks:
bw-services:
aliases:
- mypassbolt
environment:
- APP_FULL_BASE_URL=https://www.example.com # replace with your URL
- PASSBOLT_SSL_FORCE=false
- DATASOURCES_DEFAULT_HOST=mydb
- DATASOURCES_DEFAULT_DATABASE=${PASSBOLT_DATABASE:-passboltdb}
- DATASOURCES_DEFAULT_USERNAME=${PASSBOLT_USER:-user}
- DATASOURCES_DEFAULT_PASSWORD=${PASSBOLT_PASSWORD:-secret} # set a stronger password in a .env file (must match MYSQL_PASSWORD)
volumes:
- gpg_volume:/etc/passbolt/gpg
- jwt_volume:/etc/passbolt/jwt
command:
[
"/usr/bin/wait-for.sh",
@ -15,36 +29,19 @@ services:
"--",
"/docker-entrypoint.sh",
]
networks:
bw-services:
aliases:
- mypassbolt
environment:
- DATASOURCES_DEFAULT_HOST=mydb
- DATASOURCES_DEFAULT_PASSWORD=db-user-pwd # replace with a stronger password (must match MYSQL_PASSWORD)
- DATASOURCES_DEFAULT_USERNAME=user
- DATASOURCES_DEFAULT_DATABASE=passbolt
- APP_FULL_BASE_URL=https://www.example.com # replace with your URL
labels:
- bunkerweb.SERVER_NAME=www.example.com
- bunkerweb.ALLOWED_METHODS=GET|POST|HEAD|PUT|DELETE
- bunkerweb.COOKIE_FLAGS=* SameSite=Lax
- bunkerweb.USE_REVERSE_PROXY=yes
- bunkerweb.REVERSE_PROXY_URL=/
- bunkerweb.REVERSE_PROXY_HOST=https://mypassbolt
mydb:
image: mariadb
volumes:
- ./db-data:/var/lib/mysql
networks:
bw-services:
aliases:
- mydb
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=passbolt
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password (must match DATASOURCES_DEFAULT_PASSWORD)
# For the database, you can refer to the autoconf integration example including a database
volumes:
gpg_volume:
jwt_volume:
networks:
bw-services:

View File

@ -1,8 +1,12 @@
version: "3"
x-bunkerweb-env:
&bunkerweb-env
DATABASE_URI: "mariadb+pymysql://${PASSBOLT_USER:-user}:${PASSBOLT_PASSWORD:-secret}@mydb:3306/${BUNKERWEB_DATABASE:-bunkerweb}"
services:
mybunker:
image: bunkerity/bunkerweb:1.4.3
image: bunkerity/bunkerweb:1.5.0
ports:
- 80:8080
- 443:8443
@ -13,24 +17,68 @@ services:
# another example for existing folder : chown -R root:101 folder && chmod -R 770 folder
# more info at https://docs.bunkerweb.io
volumes:
- bw_data:/data
- bw-data:/data
environment:
- SERVER_NAME=www.example.com # replace with your domain
- AUTO_LETS_ENCRYPT=yes
- DISABLE_DEFAULT_SERVER=yes
- ALLOWED_METHODS=GET|POST|HEAD|PUT|DELETE
- SERVE_FILES=no
- USE_CLIENT_CACHE=yes
- USE_GZIP=yes
- USE_REVERSE_PROXY=yes
- REVERSE_PROXY_URL=/
- REVERSE_PROXY_HOST=https://mypassbolt
<<: *bunkerweb-env
SERVER_NAME: "www.example.com" # replace with your domain
API_WHITELIST_IP: "127.0.0.0/8 10.20.30.0/24"
AUTO_LETS_ENCRYPT: "yes"
COOKIE_FLAGS: "* SameSite=Lax"
DISABLE_DEFAULT_SERVER: "yes"
ALLOWED_METHODS: "GET|POST|HEAD|PUT|DELETE"
SERVE_FILES: "no"
USE_CLIENT_CACHE: "yes"
USE_GZIP: "yes"
USE_REVERSE_PROXY: "yes"
REVERSE_PROXY_URL: "/"
REVERSE_PROXY_HOST: "https://mypassbolt"
labels:
- "bunkerweb.INSTANCE" # required for the scheduler to recognize the container
networks:
- bw-universe
- bw-services
bw-scheduler:
image: bunkerity/bunkerweb-scheduler:1.5.0
depends_on:
- mybunker
environment:
<<: *bunkerweb-env
DOCKER_HOST: "tcp://docker-proxy:2375"
volumes:
- bw-data:/data
networks:
- bw-universe
- net-docker
docker-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1
networks:
- net-docker
# you will need to add a user by hand
# example : docker-compose exec mypassbolt su -m -c "bin/cake passbolt register_user -u your@email.com -f yourname -l surname -r admin" -s /bin/sh www-data
# example : docker-compose exec mypassbolt su -m -c "/usr/share/php/passbolt/bin/cake passbolt register_user -u <your@email.com> -f <yourname> -l <surname> -r admin" -s /bin/sh www-data
# more info at https://github.com/passbolt/passbolt_docker
mypassbolt:
image: passbolt/passbolt
image: passbolt/passbolt:3.8.1-1-ce
#Alternatively you can use rootless:
# image: passbolt/passbolt:3.8.1-1-ce-non-root
depends_on:
- mydb
environment:
- APP_FULL_BASE_URL=https://www.example.com # replace with your URL
- PASSBOLT_SSL_FORCE=false
- DATASOURCES_DEFAULT_HOST=mydb
- DATASOURCES_DEFAULT_DATABASE=${PASSBOLT_DATABASE:-passboltdb}
- DATASOURCES_DEFAULT_USERNAME=${PASSBOLT_USER:-user}
- DATASOURCES_DEFAULT_PASSWORD=${PASSBOLT_PASSWORD:-secret} # set a stronger password in a .env file (must match MYSQL_PASSWORD)
volumes:
- gpg_volume:/etc/passbolt/gpg
- jwt_volume:/etc/passbolt/jwt
command:
[
"/usr/bin/wait-for.sh",
@ -38,24 +86,34 @@ services:
"0",
"mydb:3306",
"--",
"/docker-entrypoint.sh",
"/docker-entrypoint.sh"
]
environment:
- DATASOURCES_DEFAULT_HOST=mydb
- DATASOURCES_DEFAULT_PASSWORD=db-user-pwd # replace with a stronger password (must match MYSQL_PASSWORD)
- DATASOURCES_DEFAULT_USERNAME=user
- DATASOURCES_DEFAULT_DATABASE=passbolt
- APP_FULL_BASE_URL=https://www.example.com # replace with your URL
networks:
- bw-services
mydb:
image: mariadb
volumes:
- ./db-data:/var/lib/mysql
- db-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=passbolt
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password (must match DATASOURCES_DEFAULT_PASSWORD)
MARIADB_RANDOM_ROOT_PASSWORD: "yes"
entrypoint: sh -c "echo 'DROP USER IF EXISTS \"${PASSBOLT_USER:-user}\"; CREATE USER \"${PASSBOLT_USER:-user}\"@\"%\"; CREATE DATABASE IF NOT EXISTS ${PASSBOLT_DATABASE:-passboltdb}; CREATE DATABASE IF NOT EXISTS ${BUNKERWEB_DATABASE:-bunkerweb}; GRANT ALL PRIVILEGES ON ${PASSBOLT_DATABASE:-passboltdb}.* TO \"${PASSBOLT_USER:-user}\"@\"%\" IDENTIFIED BY \"${PASSBOLT_PASSWORD:-secret}\"; GRANT ALL PRIVILEGES ON ${BUNKERWEB_DATABASE:-bunkerweb}.* TO \"${PASSBOLT_USER:-user}\"@\"%\" IDENTIFIED BY \"${PASSBOLT_PASSWORD:-secret}\"; FLUSH PRIVILEGES;' > /docker-entrypoint-initdb.d/init.sql; /usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
networks:
- bw-universe
- bw-services
volumes:
bw_data:
gpg_volume:
jwt_volume:
db-data:
bw-data:
networks:
bw-universe:
ipam:
driver: default
config:
- subnet: 10.20.30.0/24
bw-services:
net-docker:

View File

@ -2,10 +2,22 @@ version: "3"
services:
# you will need to add a user by hand
# example : docker-compose exec mypassbolt su -m -c "bin/cake passbolt register_user -u your@email.com -f yourname -l surname -r admin" -s /bin/sh www-data
# example : docker-compose exec mypassbolt su -m -c "/usr/share/php/passbolt/bin/cake passbolt register_user -u <your@email.com> -f <yourname> -l <surname> -r admin" -s /bin/sh www-data
# more info at https://github.com/passbolt/passbolt_docker
mypassbolt:
image: passbolt/passbolt
image: passbolt/passbolt:3.8.1-1-ce
networks:
- bw-services
environment:
- APP_FULL_BASE_URL=https://www.example.com # replace with your URL
- PASSBOLT_SSL_FORCE=false
- DATASOURCES_DEFAULT_HOST=mydb
- DATASOURCES_DEFAULT_DATABASE=${PASSBOLT_DATABASE:-passboltdb}
- DATASOURCES_DEFAULT_USERNAME=${PASSBOLT_USER:-user}
- DATASOURCES_DEFAULT_PASSWORD=${PASSBOLT_PASSWORD:-secret} # set a stronger password in a .env file (must match MYSQL_PASSWORD)
volumes:
- gpg_volume:/etc/passbolt/gpg
- jwt_volume:/etc/passbolt/jwt
command:
[
"/usr/bin/wait-for.sh",
@ -15,14 +27,6 @@ services:
"--",
"/docker-entrypoint.sh",
]
networks:
- bw-services
environment:
- DATASOURCES_DEFAULT_HOST=mydb
- DATASOURCES_DEFAULT_PASSWORD=db-user-pwd # replace with a stronger password (must match MYSQL_PASSWORD)
- DATASOURCES_DEFAULT_USERNAME=user
- DATASOURCES_DEFAULT_DATABASE=passbolt
- APP_FULL_BASE_URL=https://www.example.com # replace with your URL
deploy:
placement:
constraints:
@ -30,25 +34,12 @@ services:
labels:
- bunkerweb.SERVER_NAME=www.example.com
- bunkerweb.ALLOWED_METHODS=GET|POST|HEAD|PUT|DELETE
- bunkerweb.COOKIE_FLAGS=* SameSite=Lax
- bunkerweb.USE_REVERSE_PROXY=yes
- bunkerweb.REVERSE_PROXY_URL=/
- bunkerweb.REVERSE_PROXY_HOST=https://mypassbolt
mydb:
image: mariadb
volumes:
- db_data:/var/lib/mysql
networks:
- bw-services
environment:
- MYSQL_ROOT_PASSWORD=db-root-pwd # replace with a stronger password
- MYSQL_DATABASE=passbolt
- MYSQL_USER=user
- MYSQL_PASSWORD=db-user-pwd # replace with a stronger password (must match DATASOURCES_DEFAULT_PASSWORD)
deploy:
placement:
constraints:
- "node.role==worker"
# For the database, you can refer to the swarm integration example including a database
networks:
bw-services:

View File

@ -1224,14 +1224,18 @@ class Database:
"every": job.every,
"reload": job.reload,
"success": job.success,
"last_run": job.last_run.strftime("%Y/%m/%d, %I:%M:%S %p"),
"last_run": job.last_run.strftime("%Y/%m/%d, %I:%M:%S %p")
if job.last_run is not None
else "Never",
"cache": [
{
"service_id": cache.service_id,
"file_name": cache.file_name,
"last_update": cache.last_update.strftime(
"%Y/%m/%d, %I:%M:%S %p"
),
)
if cache.last_update is not None
else "Never",
}
for cache in session.query(Jobs_cache)
.with_entities(

View File

@ -105,6 +105,7 @@ class JobScheduler(ApiCaller):
self.__logger.info(
f"Executing job {name} from plugin {plugin} ...",
)
success = True
try:
proc = run(
f"{path}/jobs/{file}",
@ -115,6 +116,7 @@ class JobScheduler(ApiCaller):
group=101,
)
except BaseException:
success = False
with self.__thread_lock:
self.__logger.error(
f"Exception while executing job {name} from plugin {plugin} :\n{format_exc()}",
@ -122,23 +124,23 @@ class JobScheduler(ApiCaller):
self.__job_success = False
if self.__job_success and proc.returncode >= 2:
success = False
with self.__thread_lock:
self.__logger.error(
f"Error while executing job {name} from plugin {plugin}",
)
self.__job_success = False
with self.__thread_lock:
err = self.__db.update_job(plugin, name, self.__job_success)
err = self.__db.update_job(plugin, name, success)
if not err:
self.__logger.info(
f"Successfully updated database for the job {name} from plugin {plugin}",
)
else:
self.__logger.warning(
f"Failed to update database for the job {name} from plugin {plugin}: {err}",
)
if not err:
self.__logger.info(
f"Successfully updated database for the job {name} from plugin {plugin}",
)
else:
self.__logger.warning(
f"Failed to update database for the job {name} from plugin {plugin}: {err}",
)
def setup(self):
for plugin, jobs in self.__jobs.items():

View File

@ -22,6 +22,7 @@ from flask import (
from flask_login import LoginManager, login_required, login_user, logout_user
from flask_wtf.csrf import CSRFProtect, CSRFError, generate_csrf
from json import JSONDecodeError, dumps, load as json_load
from jinja2 import Template
from kubernetes import client as kube_client
from kubernetes.client.exceptions import ApiException as kube_ApiException
from os import chmod, getenv, getpid, listdir, mkdir, walk
@ -152,7 +153,7 @@ try:
WTF_CSRF_SSL_STRICT=False,
USER=user,
SEND_FILE_MAX_AGE_DEFAULT=86400,
PLUGIN_ARGS=None,
PLUGIN_ARGS={},
RELOADING=False,
TO_FLASH=[],
DARK_MODE=False,
@ -274,44 +275,44 @@ def home():
if r and r.status_code == 200:
remote_version = r.text.strip()
headers = default_headers()
headers.update({"User-Agent": "bunkerweb-ui"})
# headers = default_headers()
# headers.update({"User-Agent": "bunkerweb-ui"})
try:
r = get(
"https://www.bunkerity.com/wp-json/wp/v2/posts",
headers=headers,
)
except BaseException:
r = None
# try:
# r = get(
# "https://www.bunkerity.com/wp-json/wp/v2/posts",
# headers=headers,
# )
# except BaseException:
# r = None
formatted_posts = None
if r and r.status_code == 200:
posts = r.json()
formatted_posts = []
# formatted_posts = None
# if r and r.status_code == 200:
# posts = r.json()
# formatted_posts = []
for post in posts[:5]:
formatted_posts.append(
{
"link": post["link"],
"title": post["title"]["rendered"],
"description": BeautifulSoup(
post["content"]["rendered"][
post["content"]["rendered"].index("<em>")
+ 4 : post["content"]["rendered"].index("</em>")
],
features="html.parser",
).get_text()[:256]
+ ("..." if len(post["content"]["rendered"]) > 256 else ""),
"date": dateutil_parse(post["date"]).strftime("%B %d, %Y"),
"image_url": post["yoast_head_json"]["og_image"][0]["url"].replace(
"wwwdev", "www"
),
"reading_time": post["yoast_head_json"]["twitter_misc"][
"Est. reading time"
],
}
)
# for post in posts[:5]:
# formatted_posts.append(
# {
# "link": post["link"],
# "title": post["title"]["rendered"],
# "description": BeautifulSoup(
# post["content"]["rendered"][
# post["content"]["rendered"].index("<em>")
# + 4 : post["content"]["rendered"].index("</em>")
# ],
# features="html.parser",
# ).get_text()[:256]
# + ("..." if len(post["content"]["rendered"]) > 256 else ""),
# "date": dateutil_parse(post["date"]).strftime("%B %d, %Y"),
# "image_url": post["yoast_head_json"]["og_image"][0]["url"].replace(
# "wwwdev", "www"
# ),
# "reading_time": post["yoast_head_json"]["twitter_misc"][
# "Est. reading time"
# ],
# }
# )
instances_number = len(app.config["INSTANCES"].get_instances())
services_number = len(app.config["CONFIG"].get_services())
@ -323,7 +324,7 @@ def home():
version=bw_version,
instances_number=instances_number,
services_number=services_number,
posts=formatted_posts,
# posts=formatted_posts,
plugins_errors=db.get_plugins_errors(),
dark_mode=app.config["DARK_MODE"],
)
@ -958,12 +959,14 @@ def plugins():
# Fix permissions for plugins folders
for root, dirs, files in walk("/etc/bunkerweb/plugins", topdown=False):
for name in files + dirs:
chown(join(root, name), "nginx", "nginx")
chown(join(root, name), 101, 101)
chmod(join(root, name), 0o770)
if operation:
flash(operation)
app.config["CONFIG"].reload_plugins()
# Reload instances
app.config["RELOADING"] = True
Thread(
@ -979,7 +982,6 @@ def plugins():
except OSError:
pass
app.config["CONFIG"].reload_plugins()
return redirect(
url_for("loading", next=url_for("plugins"), message="Reloading plugins")
)
@ -999,8 +1001,10 @@ def plugins():
flash(f"Plugin {plugin_id} not found", "error")
if page_path:
return render_template(
page_path,
with open(page_path, "r") as f:
template = Template(f.read())
return template.render(
csrf_token=generate_csrf,
url_for=url_for,
dark_mode=app.config["DARK_MODE"],
@ -1011,8 +1015,23 @@ def plugins():
),
)
app.config["CONFIG"].reload_plugins()
plugins = app.config["CONFIG"].get_plugins()
plugins_internal = 0
plugins_external = 0
for plugin in plugins:
if plugin["external"] is True:
plugins_external += 1
else:
plugins_internal += 1
return render_template(
"plugins.html",
plugins=plugins,
plugins_internal=plugins_internal,
plugins_external=plugins_external,
plugins_errors=db.get_plugins_errors(),
dark_mode=app.config["DARK_MODE"],
)
@ -1047,7 +1066,7 @@ def custom_plugin(plugin):
f"Invalid plugin id, <b>{plugin}</b> (must be between 1 and 64 characters, only letters, numbers, underscores and hyphens)",
"error",
)
return redirect(url_for("loading", next=url_for("plugins")))
return redirect(url_for("loading", next=url_for("plugins", plugin_id=plugin)))
if not exists(f"/etc/bunkerweb/plugins/{plugin}/ui/actions.py") and not exists(
f"/usr/share/bunkerweb/core/{plugin}/ui/actions.py"
@ -1056,7 +1075,7 @@ def custom_plugin(plugin):
f"The <i>actions.py</i> file for the plugin <b>{plugin}</b> does not exist",
"error",
)
return redirect(url_for("loading", next=url_for("plugins")))
return redirect(url_for("loading", next=url_for("plugins", plugin_id=plugin)))
# Add the custom plugin to sys.path
sys_path.append(
@ -1075,7 +1094,7 @@ def custom_plugin(plugin):
f"An error occurred while importing the plugin <b>{plugin}</b>:<br/>{format_exc()}",
"error",
)
return redirect(url_for("loading", next=url_for("plugins")))
return redirect(url_for("loading", next=url_for("plugins", plugin_id=plugin)))
error = False
res = None
@ -1090,7 +1109,7 @@ def custom_plugin(plugin):
"error",
)
error = True
return redirect(url_for("loading", next=url_for("plugins")))
return redirect(url_for("loading", next=url_for("plugins", plugin_id=plugin)))
except:
flash(
f"An error occurred while executing the plugin <b>{plugin}</b>:<br/>{format_exc()}",
@ -1109,12 +1128,14 @@ def custom_plugin(plugin):
or res is None
or isinstance(res, dict) is False
):
return redirect(url_for("loading", next=url_for("plugins")))
return redirect(
url_for("loading", next=url_for("plugins", plugin_id=plugin))
)
app.config["PLUGIN_ARGS"] = {"plugin": plugin, "args": res}
flash(f"Your action <b>{plugin}</b> has been executed")
return redirect(url_for("loading", next=url_for("plugins")))
return redirect(url_for("loading", next=url_for("plugins", plugin_id=plugin)))
@app.route("/cache", methods=["GET"])
@ -1383,6 +1404,7 @@ def jobs():
return render_template(
"jobs.html",
jobs=db.get_jobs(),
jobs_errors=db.get_plugins_errors(),
dark_mode=app.config["DARK_MODE"],
)
@ -1454,13 +1476,15 @@ def login():
@app.route("/darkmode", methods=["POST"])
@login_required
def darkmode():
if "darkmode" in request.form:
if request.form["darkmode"] == "true":
app.config["DARK_MODE"] = True
else:
app.config["DARK_MODE"] = False
if not request.is_json:
return jsonify({"status": "ko", "message": "invalid request"}), 400
return jsonify({"status": "ok"})
if "darkmode" in request.json:
app.config["DARK_MODE"] = request.json["darkmode"] == "true"
else:
return jsonify({"status": "ko", "message": "darkmode is required"}), 422
return jsonify({"status": "ok"}), 200
@app.route("/check_reloading")

View File

@ -38,6 +38,7 @@ class Config:
def reload_plugins(self) -> None:
self.__plugins = []
external_plugins = []
for foldername in list(iglob("/etc/bunkerweb/plugins/*")) + list(
iglob("/usr/share/bunkerweb/core/*")
@ -55,6 +56,13 @@ class Config:
"external": foldername.startswith("/etc/bunkerweb/plugins"),
}
)
if plugin["external"] is True:
external_plugin = deepcopy(plugin)
del external_plugin["external"]
del external_plugin["page"]
external_plugins.append(external_plugin)
if "ui" in content:
if "template.html" in listdir(f"{foldername}/ui"):
plugin["page"] = True
@ -67,6 +75,13 @@ class Config:
**self.__settings,
}
if external_plugins:
err = self.__db.update_external_plugins(external_plugins)
if err:
self.__logger.error(
f"Couldn't update external plugins to database: {err}",
)
def __env_to_dict(self, filename: str) -> dict:
"""Converts the content of an env file into a dict

View File

@ -775,6 +775,10 @@ h6 {
z-index: 100;
}
.z-\[10000\] {
z-index: 10000;
}
.z-\[1001\] {
z-index: 1001;
}
@ -791,10 +795,6 @@ h6 {
z-index: -10;
}
.z-10 {
z-index: 10;
}
.z-990 {
z-index: 990;
}
@ -807,16 +807,16 @@ h6 {
z-index: 1020;
}
.z-10 {
z-index: 10;
}
.z-50 {
z-index: 50;
}
.z-\[1500\] {
z-index: 1500;
}
.z-\[10000\] {
z-index: 10000;
.z-0 {
z-index: 0;
}
.order-2 {
@ -1022,14 +1022,6 @@ h6 {
margin-bottom: 2rem;
}
.mb-7 {
margin-bottom: 1.75rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.mt-2 {
margin-top: 0.5rem;
}
@ -1058,6 +1050,14 @@ h6 {
margin-top: 1.5rem;
}
.mb-7 {
margin-bottom: 1.75rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.mr-6 {
margin-right: 1.5rem;
}
@ -1114,6 +1114,14 @@ h6 {
height: 100%;
}
.h-screen {
height: 100vh;
}
.h-12 {
height: 3rem;
}
.h-4 {
height: 1rem;
}
@ -1134,26 +1142,10 @@ h6 {
height: 1.25rem;
}
.h-screen {
height: 100vh;
}
.h-48 {
height: 12rem;
}
.h-12 {
height: 3rem;
}
.h-16 {
height: 4rem;
}
.h-3 {
height: 0.75rem;
}
.h-19 {
height: 4.75rem;
}
@ -1174,6 +1166,10 @@ h6 {
height: 7.5rem;
}
.h-3 {
height: 0.75rem;
}
.h-\[90vh\] {
height: 90vh;
}
@ -1258,6 +1254,14 @@ h6 {
width: 100%;
}
.w-screen {
width: 100vw;
}
.w-40 {
width: 10rem;
}
.w-4 {
width: 1rem;
}
@ -1278,10 +1282,6 @@ h6 {
width: 12rem;
}
.w-screen {
width: 100vw;
}
.w-12 {
width: 3rem;
}
@ -1290,14 +1290,6 @@ h6 {
width: 1.25rem;
}
.w-60 {
width: 15rem;
}
.w-3 {
width: 0.75rem;
}
.w-28 {
width: 7rem;
}
@ -1314,12 +1306,12 @@ h6 {
width: 22.5rem;
}
.w-80 {
width: 20rem;
.w-3 {
width: 0.75rem;
}
.w-40 {
width: 10rem;
.w-80 {
width: 20rem;
}
.min-w-0 {
@ -1432,11 +1424,6 @@ h6 {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-x-1 {
--tw-translate-x: 0.25rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.-translate-x-full {
--tw-translate-x: -100%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -1452,6 +1439,11 @@ h6 {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-x-1 {
--tw-translate-x: 0.25rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.-translate-y-7 {
--tw-translate-y: -1.75rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -1535,10 +1527,6 @@ h6 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
@ -1652,6 +1640,10 @@ h6 {
border-radius: 50%;
}
.rounded-10 {
border-radius: 2.5rem;
}
.rounded-1\.4 {
border-radius: 0.35rem;
}
@ -1660,10 +1652,6 @@ h6 {
border-radius: 0.25rem;
}
.rounded-10 {
border-radius: 2.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
@ -2465,6 +2453,11 @@ h6 {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.brightness-75 {
--tw-brightness: brightness(.75);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
@ -2481,12 +2474,6 @@ h6 {
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 150ms;
}
.transition {
transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@ -2495,20 +2482,26 @@ h6 {
transition-duration: 150ms;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 150ms;
}
.transition-transform {
transition-property: transform;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 150ms;
}
.duration-200 {
transition-duration: 200ms;
}
.duration-300 {
transition-duration: 300ms;
}
.duration-200 {
transition-duration: 200ms;
}
.duration-250 {
transition-duration: 250ms;
}
@ -3138,14 +3131,6 @@ h6 {
grid-column: span 4 / span 4;
}
.sm\:col-span-1 {
grid-column: span 1 / span 1;
}
.sm\:col-span-2 {
grid-column: span 2 / span 2;
}
.sm\:col-start-5 {
grid-column-start: 5;
}
@ -3172,8 +3157,8 @@ h6 {
display: none;
}
.sm\:h-24 {
height: 6rem;
.sm\:h-14 {
height: 3.5rem;
}
.sm\:h-10 {
@ -3184,18 +3169,6 @@ h6 {
height: 1.75rem;
}
.sm\:h-16 {
height: 4rem;
}
.sm\:h-20 {
height: 5rem;
}
.sm\:h-14 {
height: 3.5rem;
}
.sm\:max-h-28 {
max-height: 7rem;
}
@ -3204,8 +3177,8 @@ h6 {
max-height: 31.25rem;
}
.sm\:w-80 {
width: 20rem;
.sm\:w-50 {
width: 12.5rem;
}
.sm\:w-36 {
@ -3216,14 +3189,6 @@ h6 {
width: 1.75rem;
}
.sm\:w-60 {
width: 15rem;
}
.sm\:w-50 {
width: 12.5rem;
}
.sm\:flex-row {
flex-direction: row;
}
@ -3322,10 +3287,6 @@ h6 {
display: none;
}
.md\:h-25 {
height: 6.25rem;
}
.md\:h-16 {
height: 4rem;
}
@ -3338,18 +3299,14 @@ h6 {
min-height: 75vh;
}
.md\:w-1\/2 {
width: 50%;
}
.md\:w-80 {
width: 20rem;
}
.md\:w-60 {
width: 15rem;
}
.md\:w-1\/2 {
width: 50%;
}
.md\:justify-end {
justify-content: flex-end;
}
@ -3438,38 +3395,26 @@ h6 {
display: none;
}
.lg\:h-24 {
height: 6rem;
}
.lg\:h-9 {
height: 2.25rem;
}
.lg\:h-30 {
height: 7.5rem;
}
.lg\:h-20 {
height: 5rem;
}
.lg\:h-24 {
height: 6rem;
.lg\:w-80 {
width: 20rem;
}
.lg\:w-9 {
width: 2.25rem;
}
.lg\:w-90 {
width: 22.5rem;
}
.lg\:w-1\/2 {
width: 50%;
}
.lg\:w-80 {
width: 20rem;
}
.lg\:flex-none {
flex: none;
}
@ -3521,10 +3466,6 @@ h6 {
top: 0.75rem;
}
.xl\:col-span-4 {
grid-column: span 4 / span 4;
}
.xl\:mx-4 {
margin-left: 1rem;
margin-right: 1rem;
@ -3613,14 +3554,6 @@ h6 {
.\33xl\:col-span-4 {
grid-column: span 4 / span 4;
}
.\33xl\:col-span-6 {
grid-column: span 6 / span 6;
}
.\33xl\:col-span-2 {
grid-column: span 2 / span 2;
}
}
.\[\&\>\*\]\:bg-primary>* {

View File

@ -3,7 +3,7 @@ import {
FolderEditor,
FolderModal,
FolderDropdown,
} from "./utils.js";
} from "./utils/file.manager.js";
const setModal = new FolderModal("cache");
const setEditor = new FolderEditor();

View File

@ -3,7 +3,7 @@ import {
FolderEditor,
FolderModal,
FolderDropdown,
} from "./utils.js";
} from "./utils/file.manager.js";
const setModal = new FolderModal("configs");
const setEditor = new FolderEditor();

View File

@ -1,4 +1,4 @@
import { Checkbox, Loader } from "./utils.js";
import { Checkbox } from "./utils/form.js";
class Menu {
constructor() {
@ -69,10 +69,11 @@ class darkMode {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-CSRF-Token": this.csrf.value,
},
body: JSON.stringify({ darkmode: isDark, csrf_token: this.csrf.value }),
body: JSON.stringify({ darkmode: isDark }),
};
const send = await fetch(`${location.href}/darkmode}`, data);
const send = await fetch(`${location.href.split("/").slice(0, -1).join("/")}/darkmode`, data);
}
}
@ -97,6 +98,43 @@ class FlashMsg {
}
}
class Loader {
constructor() {
this.menuContainer = document.querySelector("[menu-container]");
this.logoContainer = document.querySelector("[loader]");
this.logoEl = document.querySelector("[loader-img]");
this.isLoading = true;
this.init();
}
init() {
this.loading();
window.addEventListener("load", (e) => {
setTimeout(() => {
this.logoContainer.classList.add("opacity-0");
}, 350);
setTimeout(() => {
this.isLoading = false;
this.logoContainer.classList.add("hidden");
}, 650);
setTimeout(() => {
this.logoContainer.remove();
}, 800);
});
}
loading() {
if ((this.isLoading = true)) {
setTimeout(() => {
this.logoEl.classList.toggle("scale-105");
this.loading();
}, 300);
}
}
}
const setLoader = new Loader();
const setMenu = new Menu();
const setNews = new News();

View File

@ -1,7 +1,114 @@
import { Checkbox, Popover, Select, Tabs, FormatValue } from "./utils.js";
import { Checkbox, Select } from "./utils/form.js";
import { Popover, Tabs, FormatValue } from "./utils/settings.js";
class FilterSettings {
constructor(prefix) {
this.prefix = prefix;
this.input = document.querySelector("input#settings-filter");
//DESKTOP
this.deskTabs = document.querySelectorAll(`[${this.prefix}-item-handler]`);
this.init();
}
init() {
this.input.addEventListener("input", () => {
this.resetFilter();
//get inp format
const inpValue = this.input.value.trim().toLowerCase();
//loop all tabs
this.deskTabs.forEach((tab) => {
//get settings of tabs except multiples
const settings = this.getSettingsFromTab(tab);
//compare total count to currCount to determine
//if tabs need to be hidden
const settingCount = settings.length;
let hiddenCount = 0;
settings.forEach((setting) => {
console.log(setting);
try {
const title = setting
.querySelector("h5")
.textContent.trim()
.toLowerCase();
if (!title.includes(inpValue)) {
setting.classList.add("hidden");
hiddenCount++;
}
} catch (err) {}
});
//case no setting match, hidden tab and content
if (settingCount === hiddenCount) {
const tabName = tab.getAttribute(`${this.prefix}-item-handler`);
//hide mobile and desk tabs
tab.classList.add("hidden");
document
.querySelector(`[${this.prefix}-mobile-item-handler="${tabName}"]`)
.classList.add("hidden");
document
.querySelector(`[${this.prefix}-item=${tabName}]`)
.querySelector("[setting-header]")
.classList.add("hidden");
}
});
});
}
resetFilter() {
this.deskTabs.forEach((tab) => {
const tabName = tab.getAttribute(`${this.prefix}-item-handler`);
//hide mobile and desk tabs
tab.classList.remove("hidden");
document
.querySelector(`[${this.prefix}-mobile-item-handler="${tabName}"]`)
.classList.remove("hidden");
document
.querySelector(`[${this.prefix}-item=${tabName}]`)
.querySelector("[setting-header]")
.classList.remove("hidden");
const settings = this.getSettingsFromTab(tab);
settings.forEach((setting) => {
setting.classList.remove("hidden");
});
});
}
getSettingsFromTab(tabEl) {
const tabName = tabEl.getAttribute(`${this.prefix}-item-handler`);
const settingContainer = document
.querySelector(`[${this.prefix}-item="${tabName}"]`)
.querySelector(`[${this.prefix}-settings]`);
const settings = settingContainer.querySelectorAll("[setting-container]");
return settings;
}
}
class Multiple {
constructor(prefix) {
this.prefix = prefix;
this.init();
}
//hide multiples handler if no multiple setting on plugin
init() {
//hide multiple btn if no multiple exist on a plugin
const multiples = document.querySelectorAll(
`[${this.prefix}-settings-multiple]`
);
multiples.forEach((container) => {
console.log(container.querySelectorAll(`[setting-container]`));
if (container.querySelectorAll(`[setting-container]`).length <= 0)
container.parentElement
.querySelector("[multiple-handler]")
.classList.add("hidden");
});
}
}
const setCheckbox = new Checkbox("[global-config-form]");
const setSelect = new Select("[global-config-form]", "global-config");
const setPopover = new Popover("main", "global-config");
const setTabs = new Tabs("[global-config-tabs]", "global-config");
const format = new FormatValue();
const setMultiple = new Multiple("global-config");
const setFilter = new FilterSettings("global-config");

View File

@ -1,4 +1,4 @@
import { Checkbox } from "./utils.js";
import { Checkbox } from "./utils/form.js";
import Datepicker from "./datepicker/datepicker.js";
class Dropdown {
@ -427,7 +427,6 @@ class Filter {
class LogsDate {
constructor(el, options = {}) {
this.datepicker = new Datepicker(el, options);
this.init();
this.container = document.querySelector("[logs-settings]");
}
}

View File

@ -1,4 +1,5 @@
import { Checkbox, Popover, Select, Tabs, FormatValue } from "./utils.js";
import { Checkbox, Select } from "./utils/form.js";
import { Popover, Tabs, FormatValue } from "./utils/settings.js";
class ServiceModal {
constructor() {
@ -15,6 +16,8 @@ class ServiceModal {
//general inputs
this.inputs = this.modal.querySelectorAll("input[default-value]");
this.selects = this.modal.querySelectorAll("select[default-value]");
this.lastGroup = "";
this.init();
}
@ -40,7 +43,7 @@ class ServiceModal {
) {
this.setDeleteForm(
"delete",
e.target.closest("button").getAttribute("service-name")
e.target.closest("button").getAttribute("services-name")
);
this.openModal();
}
@ -60,10 +63,15 @@ class ServiceModal {
if (
e.target.closest("button").getAttribute("services-action") === "edit"
) {
this.setNewEditForm(
"edit",
e.target.closest("button").getAttribute("service-name")
);
//no reupdate if same service
const serviceName = e.target
.closest("button")
.getAttribute("services-name");
if (this.lastGroup === serviceName) return this.openModal();
//else
this.lastGroup = serviceName;
this.setNewEditForm("edit", serviceName);
//change this to hidden config on service card later
const servicesSettings = e.target
.closest("[services-service]")
@ -238,16 +246,28 @@ class Multiple {
this.prefix = prefix;
this.container = document.querySelector("main");
this.modalForm = document.querySelector(`[${this.prefix}-modal-form]`);
this.lastGroup = "";
this.init();
}
init() {
window.addEventListener("load", () => {
this.hiddenIfNoMultiples();
});
this.container.addEventListener("click", (e) => {
//edit button
try {
if (
e.target.closest("button").getAttribute("services-action") === "edit"
) {
//avoid reupdate if same service
const serviceName = e.target
.closest("button")
.getAttribute("services-name");
if (this.lastGroup === serviceName) return;
//else
this.lastGroup = serviceName;
//remove all multiples
this.removeMultiples();
//set multiple service values
@ -274,14 +294,25 @@ class Multiple {
const multipleEls = document.querySelectorAll(
`[${this.prefix}-settings-multiple*="${serviceName}"]`
);
const count = Number(
multipleEls[1]
.getAttribute(`${this.prefix}-settings-multiple`)
.substring(
multipleEls[1].getAttribute(`${this.prefix}-settings-multiple`)
.length - 1
)
);
let count;
//case no schema
if (multipleEls.length <= 0) return;
//case only schema
if (multipleEls.length === 1) {
count = 0;
}
//case schema and custom configs with num
if (multipleEls.length > 1) {
count = Number(
multipleEls[1]
.getAttribute(`${this.prefix}-settings-multiple`)
.substring(
multipleEls[1].getAttribute(
`${this.prefix}-settings-multiple`
).length - 1
)
);
}
//the default (schema) group is the last group
const schema = document.querySelector(
`[${this.prefix}-settings-multiple="${serviceName}_SCHEMA"]`
@ -482,6 +513,20 @@ class Multiple {
//UTILS
hiddenIfNoMultiples() {
//hide multiple btn if no multiple exist on a plugin
const multiples = document.querySelectorAll(
`[${this.prefix}-settings-multiple]`
);
multiples.forEach((container) => {
console.log(container.querySelectorAll(`[setting-container]`));
if (container.querySelectorAll(`[setting-container]`).length <= 0)
container.parentElement
.querySelector("[multiple-handler]")
.classList.add("hidden");
});
}
removeMultiples() {
const multiPlugins = document.querySelectorAll(
`[${this.prefix}-settings-multiple]`
@ -514,6 +559,89 @@ class Multiple {
}
}
class FilterSettings {
constructor(prefix) {
this.prefix = prefix;
this.input = document.querySelector("input#settings-filter");
//DESKTOP
this.deskTabs = document.querySelectorAll(`[${this.prefix}-item-handler]`);
this.init();
}
init() {
this.input.addEventListener("input", () => {
this.resetFilter();
//get inp format
const inpValue = this.input.value.trim().toLowerCase();
//loop all tabs
this.deskTabs.forEach((tab) => {
//get settings of tabs except multiples
const settings = this.getSettingsFromTab(tab);
//compare total count to currCount to determine
//if tabs need to be hidden
const settingCount = settings.length;
let hiddenCount = 0;
settings.forEach((setting) => {
console.log(setting);
try {
const title = setting
.querySelector("h5")
.textContent.trim()
.toLowerCase();
if (!title.includes(inpValue)) {
setting.classList.add("hidden");
hiddenCount++;
}
} catch (err) {}
});
//case no setting match, hidden tab and content
if (settingCount === hiddenCount) {
const tabName = tab.getAttribute(`${this.prefix}-item-handler`);
//hide mobile and desk tabs
tab.classList.add("hidden");
document
.querySelector(`[${this.prefix}-mobile-item-handler="${tabName}"]`)
.classList.add("hidden");
document
.querySelector(`[${this.prefix}-item=${tabName}]`)
.querySelector("[setting-header]")
.classList.add("hidden");
}
});
});
}
resetFilter() {
this.deskTabs.forEach((tab) => {
const tabName = tab.getAttribute(`${this.prefix}-item-handler`);
//hide mobile and desk tabs
tab.classList.remove("hidden");
document
.querySelector(`[${this.prefix}-mobile-item-handler="${tabName}"]`)
.classList.remove("hidden");
document
.querySelector(`[${this.prefix}-item=${tabName}]`)
.querySelector("[setting-header]")
.classList.remove("hidden");
const settings = this.getSettingsFromTab(tab);
settings.forEach((setting) => {
setting.classList.remove("hidden");
});
});
}
getSettingsFromTab(tabEl) {
const tabName = tabEl.getAttribute(`${this.prefix}-item-handler`);
const settingContainer = document
.querySelector(`[${this.prefix}-item="${tabName}"]`)
.querySelector(`[${this.prefix}-settings]`);
const settings = settingContainer.querySelectorAll("[setting-container]");
return settings;
}
}
const setCheckbox = new Checkbox("[services-modal-form]");
const setSelect = new Select("[services-modal-form]", "services");
const setPopover = new Popover("main", "services");
@ -521,3 +649,4 @@ const setTabs = new Tabs("[services-tabs]", "services");
const setModal = new ServiceModal();
const format = new FormatValue();
const setMultiple = new Multiple("services");
const setFilter = new FilterSettings("services");

View File

@ -1,338 +1,3 @@
class Checkbox {
constructor(container) {
this.container = container;
this.checkContainer = document.querySelector(`${this.container}`);
this.init();
}
init() {
this.checkContainer.addEventListener("click", (e) => {
//checkbox click
try {
if (
e.target.closest("div").hasAttribute("checkbox-handler") &&
!e.target
.closest("div")
.querySelector('input[type="checkbox"]')
.hasAttribute("disabled")
) {
//change DOM
const checkboxEl = e.target
.closest("div")
.querySelector('input[type="checkbox"]');
checkboxEl.checked
? checkboxEl.setAttribute("value", "yes")
: checkboxEl.setAttribute("value", "no");
}
} catch (err) {}
//nested elements click
try {
if (
e.target.closest("svg").hasAttribute("checkbox-handler") &&
!e.target
.closest("div")
.querySelector('input[type="checkbox"]')
.hasAttribute("disabled")
) {
e.target
.closest("div")
.querySelector('input[type="checkbox"]')
.click();
}
} catch (err) {}
});
}
}
class Select {
constructor(container, prefixAtt) {
this.prefix = prefixAtt;
this.container = container;
this.SelectContainer = document.querySelector(`${this.container}`);
this.init();
}
init() {
this.SelectContainer.addEventListener("click", (e) => {
//SELECT BTN LOGIC
try {
if (
e.target
.closest("button")
.hasAttribute(`${this.prefix}-setting-select`) &&
!e.target.closest("button").hasAttribute(`disabled`)
) {
this.toggleSelectBtn(e);
}
} catch (err) {}
//SELECT DROPDOWN BTN LOGIC
try {
if (
e.target
.closest("button")
.hasAttribute(`${this.prefix}-setting-select-dropdown-btn`)
) {
const btn = e.target.closest("button");
const btnValue = btn.getAttribute("value");
const btnSetting = btn.getAttribute(
`${this.prefix}-setting-select-dropdown-btn`
);
//add new value to custom
const selectCustom = document.querySelector(
`[${this.prefix}-setting-select="${btnSetting}"]`
);
selectCustom.querySelector(
`[${this.prefix}-setting-select-text]`
).textContent = btnValue;
//add selected to new value
//change style
const dropdownEl = document.querySelector(
`[${this.prefix}-setting-select-dropdown="${btnSetting}"]`
);
dropdownEl.classList.add("hidden");
dropdownEl.classList.remove("flex");
//reset dropdown btns
const btnEls = dropdownEl.querySelectorAll("button");
btnEls.forEach((btn) => {
btn.classList.remove(
"dark:bg-primary",
"bg-primary",
"bg-primary",
"text-gray-300",
"text-gray-300"
);
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
});
//highlight clicked btn
btn.classList.remove(
"bg-white",
"dark:bg-slate-700",
"text-gray-700"
);
btn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
//close dropdown
const dropdownChevron = document.querySelector(
`svg[${this.prefix}-setting-select="${btnSetting}"]`
);
dropdownChevron.classList.remove("rotate-180");
//update real select element
this.updateSelected(
document.querySelector(
`[${this.prefix}-setting-select-default="${btnSetting}"]`
),
btnValue
);
}
} catch (err) {}
});
}
updateSelected(selectEl, selectedValue) {
const options = selectEl.querySelectorAll("option");
//remove selected to all
options.forEach((option) => {
option.removeAttribute("selected");
option.selected = false;
});
//select new one
const newOption = selectEl.querySelector(
`option[value="${selectedValue}"]`
);
newOption.selected = true;
newOption.setAttribute("selected", "");
}
toggleSelectBtn(e) {
const attribut = e.target
.closest("button")
.getAttribute(`${this.prefix}-setting-select`);
//toggle dropdown
const dropdownEl = document.querySelector(
`[${this.prefix}-setting-select-dropdown="${attribut}"]`
);
const dropdownChevron = document.querySelector(
`svg[${this.prefix}-setting-select="${attribut}"]`
);
dropdownEl.classList.toggle("hidden");
dropdownEl.classList.toggle("flex");
dropdownChevron.classList.toggle("rotate-180");
}
}
class Popover {
constructor(container, prefix) {
this.prefix = prefix;
this.container = container;
this.popoverContainer = document.querySelector(`${this.container}`);
this.init();
}
init() {
let popoverCount = 0; //for auto hide
let btnPopoverAtt = ""; //to manage info btn clicked
this.popoverContainer.addEventListener("click", (e) => {
//POPOVER LOGIC
try {
if (e.target.closest("svg").hasAttribute(`${this.prefix}-info-btn`)) {
const btnPop = e.target.closest("svg");
//toggle curr popover
const popover = btnPop.parentElement.querySelector(
`[${this.prefix}-info-popover]`
);
popover.classList.toggle("hidden");
//get a btn att if none
if (btnPopoverAtt === "")
btnPopoverAtt = btnPop.getAttribute(`${this.prefix}-info-btn`);
//compare prev btn and curr
//hide prev popover if not the same
if (
btnPopoverAtt !== "" &&
btnPopoverAtt !== btnPop.getAttribute(`${this.prefix}-info-btn`)
) {
const prevPopover = document.querySelector(
`[${this.prefix}-info-popover="${btnPopoverAtt}"]`
);
prevPopover.classList.add("hidden");
btnPopoverAtt = btnPop.getAttribute(`${this.prefix}-info-btn`);
}
//hide popover after an amount of time
popoverCount++;
const currCount = popoverCount;
setTimeout(() => {
//if another click on same infoBtn, restart hidden
if (currCount === popoverCount) popover.classList.add("hidden");
}, 3000);
}
} catch (err) {}
});
}
}
class Tabs {
constructor(container, prefix) {
this.prefix = prefix;
this.container = container;
this.tabsContainer = document.querySelector(`${this.container}`);
this.mobileBtn = document.querySelector(`[${this.prefix}-mobile-select]`);
this.mobileBtnTxt = this.mobileBtn.querySelector(`span`);
this.mobileBtnSVG = document.querySelector(
`[${this.prefix}-mobile-chevron]`
);
this.mobileDropdown = document.querySelector(
`[${this.prefix}-mobile-dropdown]`
);
this.mobileDropdownEls = this.mobileDropdown.querySelectorAll(`button`);
this.mobileBtn.addEventListener(`click`, this.toggleDropdown.bind(this));
//FORM
this.settingContainers = document.querySelectorAll(`[${this.prefix}-item]`);
this.generalSettings = document.querySelector(
`[${this.prefix}-item='general']`
);
this.initTabs();
this.initDisplay();
}
initTabs() {
this.tabsContainer.addEventListener("click", (e) => {
//MOBILE TABS LOGIC
try {
if (
!e.target.hasAttribute(`${this.prefix}-mobile-info-btn`) &&
e.target.hasAttribute(`${this.prefix}-mobile-item-handler`)
) {
//change text to select btn
const tab = e.target.closest("button");
const tabAtt = tab.getAttribute(`${this.prefix}-mobile-item-handler`);
this.mobileBtnTxt.textContent = tab.childNodes[0].textContent;
//reset all tabs style
this.mobileDropdownEls.forEach((item) => {
item.classList.add(
"bg-white",
"dark:bg-slate-700",
"text-gray-700"
);
item.classList.remove(
"dark:bg-primary",
"bg-primary",
"bg-primary",
"text-gray-300",
"text-gray-300"
);
});
//highlight chosen one
tab.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
tab.classList.remove(
"bg-white",
"dark:bg-slate-700",
"text-gray-700"
);
//show settings
this.showRightSetting(tabAtt);
//close dropdown
this.toggleDropdown();
}
} catch (err) {}
//DESKTOP TABS LOGIC
try {
if (
!e.target.hasAttribute(`${this.prefix}-info-btn`) &&
e.target.closest("button").hasAttribute(`${this.prefix}-item-handler`)
) {
const tab = e.target.closest("button");
const tabAtt = tab.getAttribute(`${this.prefix}-item-handler`);
this.showRightSetting(tabAtt);
}
} catch (err) {}
});
}
initDisplay() {
//show general setting or
//first setting list if doesn't exist (like in services)
//on mobile and desktop
if (this.generalSettings === null) {
//desktop
document
.querySelector(
`[${this.prefix}-tabs-desktop] [${this.prefix}-item-handler]`
)
.click();
//mobile
document
.querySelector(
`[${this.prefix}-tabs-mobile] [${this.prefix}-mobile-item-handler]`
)
.click();
this.toggleDropdown();
}
}
showRightSetting(tabAtt) {
this.settingContainers.forEach((container) => {
if (container.getAttribute(`${this.prefix}-item`) === tabAtt)
container.classList.remove("hidden");
if (container.getAttribute(`${this.prefix}-item`) !== tabAtt)
container.classList.add("hidden");
});
}
toggleDropdown() {
this.mobileDropdown.classList.toggle("hidden");
this.mobileDropdown.classList.toggle("flex");
this.mobileBtnSVG.classList.toggle("rotate-180");
}
}
class FolderNav {
constructor(prefix) {
this.prefix = prefix;
@ -899,65 +564,4 @@ class FolderModal {
}
}
class FormatValue {
constructor() {
this.inputs = document.querySelectorAll("[value]");
this.init();
}
init() {
this.inputs.forEach((inp) => {
inp.setAttribute("value", inp.getAttribute("value").trim());
});
}
}
class Loader {
constructor() {
this.menuContainer = document.querySelector("[menu-container]");
this.logoContainer = document.querySelector("[loader]");
this.logoEl = document.querySelector("[loader-img]");
this.isLoading = true;
this.init();
}
init() {
this.loading();
window.addEventListener("load", (e) => {
setTimeout(() => {
this.logoContainer.classList.add("opacity-0");
}, 350);
setTimeout(() => {
this.isLoading = false;
this.logoContainer.classList.add("hidden");
}, 650);
setTimeout(() => {
this.logoContainer.remove();
}, 800);
});
}
loading() {
if ((this.isLoading = true)) {
setTimeout(() => {
this.logoEl.classList.toggle("scale-105");
this.loading();
}, 300);
}
}
}
export {
Checkbox,
Popover,
Select,
Tabs,
FolderNav,
FolderModal,
FolderEditor,
FolderDropdown,
FormatValue,
Loader,
};
export { FolderNav, FolderModal, FolderEditor, FolderDropdown };

View File

@ -0,0 +1,167 @@
class Checkbox {
constructor(container) {
this.container = container;
this.checkContainer = document.querySelector(`${this.container}`);
this.init();
}
init() {
this.checkContainer.addEventListener("click", (e) => {
//checkbox click
try {
if (
e.target.closest("div").hasAttribute("checkbox-handler") &&
!e.target
.closest("div")
.querySelector('input[type="checkbox"]')
.hasAttribute("disabled")
) {
//change DOM
const checkboxEl = e.target
.closest("div")
.querySelector('input[type="checkbox"]');
checkboxEl.checked
? checkboxEl.setAttribute("value", "yes")
: checkboxEl.setAttribute("value", "no");
}
} catch (err) {}
//nested elements click
try {
if (
e.target.closest("svg").hasAttribute("checkbox-handler") &&
!e.target
.closest("div")
.querySelector('input[type="checkbox"]')
.hasAttribute("disabled")
) {
e.target
.closest("div")
.querySelector('input[type="checkbox"]')
.click();
}
} catch (err) {}
});
}
}
class Select {
constructor(container, prefixAtt) {
this.prefix = prefixAtt;
this.container = container;
this.SelectContainer = document.querySelector(`${this.container}`);
this.init();
}
init() {
this.SelectContainer.addEventListener("click", (e) => {
//SELECT BTN LOGIC
try {
if (
e.target
.closest("button")
.hasAttribute(`${this.prefix}-setting-select`) &&
!e.target.closest("button").hasAttribute(`disabled`)
) {
this.toggleSelectBtn(e);
}
} catch (err) {}
//SELECT DROPDOWN BTN LOGIC
try {
if (
e.target
.closest("button")
.hasAttribute(`${this.prefix}-setting-select-dropdown-btn`)
) {
const btn = e.target.closest("button");
const btnValue = btn.getAttribute("value");
const btnSetting = btn.getAttribute(
`${this.prefix}-setting-select-dropdown-btn`
);
//add new value to custom
const selectCustom = document.querySelector(
`[${this.prefix}-setting-select="${btnSetting}"]`
);
selectCustom.querySelector(
`[${this.prefix}-setting-select-text]`
).textContent = btnValue;
//add selected to new value
//change style
const dropdownEl = document.querySelector(
`[${this.prefix}-setting-select-dropdown="${btnSetting}"]`
);
dropdownEl.classList.add("hidden");
dropdownEl.classList.remove("flex");
//reset dropdown btns
const btnEls = dropdownEl.querySelectorAll("button");
btnEls.forEach((btn) => {
btn.classList.remove(
"dark:bg-primary",
"bg-primary",
"bg-primary",
"text-gray-300",
"text-gray-300"
);
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
});
//highlight clicked btn
btn.classList.remove(
"bg-white",
"dark:bg-slate-700",
"text-gray-700"
);
btn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
//close dropdown
const dropdownChevron = document.querySelector(
`svg[${this.prefix}-setting-select="${btnSetting}"]`
);
dropdownChevron.classList.remove("rotate-180");
//update real select element
this.updateSelected(
document.querySelector(
`[${this.prefix}-setting-select-default="${btnSetting}"]`
),
btnValue
);
}
} catch (err) {}
});
}
updateSelected(selectEl, selectedValue) {
const options = selectEl.querySelectorAll("option");
//remove selected to all
options.forEach((option) => {
option.removeAttribute("selected");
option.selected = false;
});
//select new one
const newOption = selectEl.querySelector(
`option[value="${selectedValue}"]`
);
newOption.selected = true;
newOption.setAttribute("selected", "");
}
toggleSelectBtn(e) {
const attribut = e.target
.closest("button")
.getAttribute(`${this.prefix}-setting-select`);
//toggle dropdown
const dropdownEl = document.querySelector(
`[${this.prefix}-setting-select-dropdown="${attribut}"]`
);
const dropdownChevron = document.querySelector(
`svg[${this.prefix}-setting-select="${attribut}"]`
);
dropdownEl.classList.toggle("hidden");
dropdownEl.classList.toggle("flex");
dropdownChevron.classList.toggle("rotate-180");
}
}
export { Checkbox, Select };

View File

@ -0,0 +1,198 @@
class Popover {
constructor(container, prefix) {
this.prefix = prefix;
this.container = container;
this.popoverContainer = document.querySelector(`${this.container}`);
this.init();
}
init() {
let popoverCount = 0; //for auto hide
let btnPopoverAtt = ""; //to manage info btn clicked
this.popoverContainer.addEventListener("click", (e) => {
//POPOVER LOGIC
try {
if (e.target.closest("svg").hasAttribute(`${this.prefix}-info-btn`)) {
const btnPop = e.target.closest("svg");
//toggle curr popover
const popover = btnPop.parentElement.querySelector(
`[${this.prefix}-info-popover]`
);
popover.classList.toggle("hidden");
//get a btn att if none
if (btnPopoverAtt === "")
btnPopoverAtt = btnPop.getAttribute(`${this.prefix}-info-btn`);
//compare prev btn and curr
//hide prev popover if not the same
if (
btnPopoverAtt !== "" &&
btnPopoverAtt !== btnPop.getAttribute(`${this.prefix}-info-btn`)
) {
const prevPopover = document.querySelector(
`[${this.prefix}-info-popover="${btnPopoverAtt}"]`
);
prevPopover.classList.add("hidden");
btnPopoverAtt = btnPop.getAttribute(`${this.prefix}-info-btn`);
}
//hide popover after an amount of time
popoverCount++;
const currCount = popoverCount;
setTimeout(() => {
//if another click on same infoBtn, restart hidden
if (currCount === popoverCount) popover.classList.add("hidden");
}, 3000);
}
} catch (err) {}
});
}
}
class Tabs {
constructor(container, prefix) {
this.prefix = prefix;
this.container = container;
this.tabsContainer = document.querySelector(`${this.container}`);
//DESKTOP
this.desktopBtns = document.querySelectorAll(
`[${this.prefix}-tabs-desktop] button`
);
console.log(this.desktopBtns);
//MOBILE
this.mobileBtn = document.querySelector(`[${this.prefix}-mobile-select]`);
this.mobileBtnTxt = this.mobileBtn.querySelector(`span`);
this.mobileBtnSVG = document.querySelector(
`[${this.prefix}-mobile-chevron]`
);
this.mobileDropdown = document.querySelector(
`[${this.prefix}-mobile-dropdown]`
);
this.mobileDropdownEls = this.mobileDropdown.querySelectorAll(`button`);
this.mobileBtn.addEventListener(`click`, this.toggleDropdown.bind(this));
//FORM
this.settingContainers = document.querySelectorAll(`[${this.prefix}-item]`);
this.generalSettings = document.querySelector(
`[${this.prefix}-item='general']`
);
this.initTabs();
this.initDisplay();
}
initTabs() {
this.tabsContainer.addEventListener("click", (e) => {
//MOBILE TABS LOGIC
try {
if (
!e.target.hasAttribute(`${this.prefix}-mobile-info-btn`) &&
e.target.hasAttribute(`${this.prefix}-mobile-item-handler`)
) {
//change text to select btn
const tab = e.target.closest("button");
const tabAtt = tab.getAttribute(`${this.prefix}-mobile-item-handler`);
this.mobileBtnTxt.textContent = tab.childNodes[0].textContent;
//reset all tabs style
this.resetMobTabStyle();
//highlight chosen one
this.highlightMobClicked(tab);
//show settings
this.showRightSetting(tabAtt);
//close dropdown
this.toggleDropdown();
}
} catch (err) {}
//DESKTOP TABS LOGIC
try {
if (
!e.target.hasAttribute(`${this.prefix}-info-btn`) &&
e.target.closest("button").hasAttribute(`${this.prefix}-item-handler`)
) {
const tab = e.target.closest("button");
const tabAtt = tab.getAttribute(`${this.prefix}-item-handler`);
//style
this.resetDeskStyle();
tab.classList.add("brightness-75");
//show content
this.showRightSetting(tabAtt);
}
} catch (err) {}
});
}
resetDeskStyle() {
this.desktopBtns.forEach((tab) => {
tab.classList.remove("brightness-75");
});
}
resetMobTabStyle() {
this.mobileDropdownEls.forEach((tab) => {
tab.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
tab.classList.remove(
"dark:bg-primary",
"bg-primary",
"bg-primary",
"text-gray-300",
"text-gray-300"
);
});
}
highlightMobClicked(tabEl) {
tabEl.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
tabEl.classList.remove("bg-white", "dark:bg-slate-700", "text-gray-700");
}
initDisplay() {
//show general setting or
//first setting list if doesn't exist (like in services)
//on mobile and desktop
if (this.generalSettings === null) {
//desktop
document
.querySelector(
`[${this.prefix}-tabs-desktop] [${this.prefix}-item-handler]`
)
.click();
//mobile
document
.querySelector(
`[${this.prefix}-tabs-mobile] [${this.prefix}-mobile-item-handler]`
)
.click();
this.toggleDropdown();
}
}
showRightSetting(tabAtt) {
this.settingContainers.forEach((container) => {
if (container.getAttribute(`${this.prefix}-item`) === tabAtt)
container.classList.remove("hidden");
if (container.getAttribute(`${this.prefix}-item`) !== tabAtt)
container.classList.add("hidden");
});
}
toggleDropdown() {
this.mobileDropdown.classList.toggle("hidden");
this.mobileDropdown.classList.toggle("flex");
this.mobileBtnSVG.classList.toggle("rotate-180");
}
}
class FormatValue {
constructor() {
this.inputs = document.querySelectorAll("[value]");
this.init();
}
init() {
this.inputs.forEach((inp) => {
inp.setAttribute("value", inp.getAttribute("value").trim());
});
}
}
export { Popover, Tabs, FormatValue };

View File

@ -1,126 +1,388 @@
<ul class="col-span-6 w-full flex justify-end items-center mb-3">
<li
conf-add-folder
class="min-h-20 hidden flex-col items-center mx-4 sm:mx-6 lg:mx-0 p-4 relative cursor-pointer hover:bg-gray-100"
>
<button type="button">
<svg class="h-7 w-7 fill-primary" viewBox="0 0 512 512">
<path
d="M512 416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32H181.5c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7H448c35.3 0 64 28.7 64 64V416zM232 376c0 13.3 10.7 24 24 24s24-10.7 24-24V312h64c13.3 0 24-10.7 24-24s-10.7-24-24-24H280V200c0-13.3-10.7-24-24-24s-24 10.7-24 24v64H168c-13.3 0-24 10.7-24 24s10.7 24 24 24h64v64z"
/>
</svg>
<p
class="pt-1 mb-0 font-sans font-semibold leading-normal uppercase text-sm"
>
ADD SERVICE
</p>
</button>
</li>
<li
conf-add-file
class="min-h-20 hidden flex-col items-center mx-4 sm:mx-6 lg:mx-0 p-4 relative cursor-pointer hover:bg-gray-100"
>
<button type="button">
<svg
class="h-7 w-7"
width="384"
height="512"
viewBox="0 0 384 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_19_2)">
<path
class="fill-primary"
d="M0 64C0 28.7 28.7 0 64 0H224V128C224 145.7 238.3 160 256 160H384V448C384 483.3 355.3 512 320 512H64C28.7 512 0 483.3 0 448V64ZM384 128H256V0L384 128Z"
fill="black"
/>
<rect
class="bg-opacity-0"
x="173"
y="197"
width="38"
height="250"
rx="19"
fill="#D9D9D9"
/>
<rect
class="bg-opacity-0"
x="67"
y="341"
width="38"
height="250"
rx="19"
transform="rotate(-90 67 341)"
fill="#D9D9D9"
/>
</g>
<defs>
<clipPath id="clip0_19_2">
<rect width="384" height="512" fill="white" />
</clipPath>
</defs>
</svg>
<p
class="pt-1 mb-0 font-sans font-semibold leading-normal uppercase text-sm"
>
ADD FILE
</p>
</button>
</li>
</ul>
<!-- dropdown actions -->
{% set current_endpoint = url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-')
%}
{% set global_config =
config["CONFIG"].get_config() %}
{% set plugins = config["CONFIG"].get_plugins() %}
<!-- plugin item -->
{% for plugin in plugins %}
<div
action-dropdown="{{child['name']}}"
class="z-100 relative h-full flex-col mt-2"
{{current_endpoint}}-item="{{plugin['id']}}"
id="{{plugin['id']}}"
class="hidden w-full"
>
{% if child['type'] == "file" %}
<button
type="button"
value="read"
action-dropdown-btn="{{child['name']}}"
class="border-t rounded-t border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem"
>
read
</button>
{% endif %} {% if child['type'] == "file" and child['can_edit'] == True %}
<button
type="button"
value="edit"
action-dropdown-btn="{{child['name']}}"
class="border-t rounded-t border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem"
>
edit
</button>
{% endif %} {% if child['type'] == "file" and child['can_download'] == True %}
<button
type="button"
value="download"
action-dropdown-btn="{{child['name']}}"
class="border-t rounded-t border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem"
>
download
</button>
{% endif %} {% if child['type'] == "folder" and child['can_edit'] == True %}
<button
type="button"
value="edit"
action-dropdown-btn="{{child['name']}}"
class="border-t rounded-t border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem"
>
edit
</button>
{% endif %} {% if child['type'] == "folder" and child['can_delete'] == True %}
<button
type="button"
value="delete"
action-dropdown-btn="{{child['name']}}"
class="border-t rounded-t border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem"
>
delete
</button>
{% endif %}
<!-- title and desc -->
<div class="col-span-12">
<h5
class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0"
>
{{plugin['name']}} <span>{{plugin['version']}}</span>
</h5>
<div
class="transition duration-300 ease-in-out dark:opacity-90 ml-2 text-sm mb-2 dark:text-gray-400"
>
{{plugin['description']}}
</div>
</div>
<!-- end title and desc -->
<!-- plugin unless multiple -->
<div {{current_endpoint}}-settings-multiple class="w-full grid grid-cols-12">
{% for setting, value in plugin["settings"].items() %}{% if current_endpoint
== "global-config" and value['context'] == "global" and not value['multiple'] or current_endpoint ==
"services" and value['context'] == "multisite" and not value['multiple']%}
<div
class=" {%if value['multiple'] %}hidden{% endif %}
mx-0 sm:mx-4 my-2 col-span-12 md:mx-6 md:my-3 md:col-span-6 2xl:mx-6 2xl:my-3 2xl:col-span-4"
id="form-edit-{{current_endpoint}}-{{ value["id"] }}">
<!-- title and info -->
<div class="flex items-center my-1 relative">
<h5
class="transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>
{{value["label"]}}
</h5>
<svg
{{current_endpoint}}-info-btn="{{ value["label"] }}"
class="cursor-pointer fill-blue-500 h-5 w-5 ml-2 hover:brightness-75"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"
/>
</svg>
<!-- popover -->
<div class="hidden transition z-50 rounded-md p-3 left-0 -translate-y-7 bottom-0 absolute bg-blue-500"
{{current_endpoint}}-info-popover="{{ value["label"] }}"
>
<p class="transition duration-300 ease-in-out dark:opacity-90 font-bold text-sm text-white m-0" >{{value['help']}}
</p>
</div>
<!-- end popover -->
</div>
<!-- end title and info -->
<!-- input -->
{% if value["type"] != "select" and value["type"] != "check" %}
<input
default-value="{{global_config[setting]['value']}}" default-method="{{global_config[setting]['method']}}"
{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} disabled {% endif %} id="{{setting}}" name="{{setting}}"
class="duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 md:py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500 disabled:bg-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 dark:disabled:text-gray-300 disabled:text-gray-700"
value="{% if global_config[setting]['value'] %} {{global_config[setting]['value']}} {% else %} {{value['default']}} {% endif %}" type="{{value['type']}}" pattern="{{value['regex']|safe}}" />
{% endif %}
<!-- end input -->
<!-- select -->
{% if value["type"] == "select" %}
<!-- default hidden-->
<select default-method="{{global_config[setting]['method']}}" default-value="{{value['default']}}"
id="{{setting}}" name="{{setting}}" {{current_endpoint}}-setting-select-default="{{value['id']}}" type="form-select" id="{{setting}}" name="{{setting}}"
class="hidden">
{% for item in value['select'] %}
<option value="{{item}}" {% if global_config[setting]['value'] and global_config[setting]['value'] == item or not global_config[setting]['value'] and value['default'] == item %} selected{% endif %}>{{item}}</option>
{% endfor %}
</select>
<!-- end default hidden-->
<!--custom-->
<div class="relative">
<button
{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} disabled {% endif %} {{current_endpoint}}-setting-select="{{value['id']}}"
default-value="{{global_config[setting]['value']}}"
type="button"
class="disabled:opacity-75 dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 focus:border-green-500 flex justify-between align-middle items-center text-left text-sm leading-5.6 ease w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 md:py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
>
{% for item in value['select'] %} {% if global_config[setting]['value'] and
global_config[setting]['value'] == item %}
<span
{{current_endpoint}}-setting-select-text="{{value['id']}}"
value="{{global_config[setting]['value']}}"
>{{global_config[setting]['value']}}</span
>
{% elif not global_config[setting]['value'] and value['default'] == item %}
<span
{{current_endpoint}}-setting-select-text="{{value['id']}}"
value="{{value['default']}}"
>{{value['default']}}</span
>
{% endif %} {% endfor %}
<!-- chevron -->
<svg
{{current_endpoint}}-setting-select="{{value['id']}}"
class="transition-transform h-4 w-4 fill-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
<!-- end chevron -->
</button>
<!-- dropdown-->
<div
{{current_endpoint}}-setting-select-dropdown="{{value['id']}}"
class="hidden z-100 absolute h-full flex-col w-full mt-2"
>
{% for item in value['select'] %} {% if global_config[setting]['value'] and
global_config[setting]['value'] == item or not global_config[setting]['value']
and value['default'] == item %}
<button
type="button"
value="{{item}}"
{{current_endpoint}}-setting-select-dropdown-btn="{{value['id']}}"
type="button"
class="{% if loop.index == 1 %} border-t rounded-t {% endif %} {% if loop.index == loop.length %}rounded-b {% endif %} border-b border-l border-r border-gray-300 hover:brightness-90 bg-primary text-white my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
{{item}}
</button>
{% else %}
<button
type="button"
value="{{item}}"
{{current_endpoint}}-setting-select-dropdown-btn="{{value['id']}}"
type="button"
class="{% if loop.index == 1 %} border-t rounded-t {% endif %} {% if loop.index == loop.length %}rounded-b {% endif %} border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
{{item}}
</button>
{% endif %} {% endfor %}
</div>
<!-- end dropdown-->
</div>
<!-- end custom-->
{% endif %}
<!-- checkbox -->
{% if value["type"] == "check" %}
<div checkbox-handler="{{value['id']}}" class="relative mb-7 md:mb-0">
<input id="{{setting}}" name="{{setting}}"
default-method="{{global_config[setting]['method']}}"
default-value="{{global_config[setting]['value']}}" {% if
global_config[setting]['method'] != 'ui' or global_config[setting]['method']
!= 'default' %} disabled {% endif %} {% if global_config[setting]['value'] and
global_config[setting]['value'] == 'yes' or not
global_config[setting]['value'] and value['default'] == 'yes' %} checked {%
endif %} id="checkbox-{{value['id']}}" class="relative {% if
global_config[setting]['method'] != 'ui' and global_config[setting]['method']
!= 'default' %} pointer-events-none {% else %} cursor-pointer {% endif %}
dark:border-slate-600 dark:bg-slate-700 z-10 checked:z-0 w-5 h-5 ease
text-base rounded-1.4 checked:bg-primary checked:border-primary
dark:checked:bg-primary dark:checked:border-primary duration-250 float-left
mt-1 appearance-none border border-gray-300 bg-white bg-contain bg-center
bg-no-repeat align-top transition-all disabled:bg-gray-400
disabled:border-gray-400 dark:disabled:bg-gray-800
dark:disabled:border-gray-800 disabled:text-gray-700
dark:disabled:text-gray-300" type="checkbox" pattern="{{value['regex']|safe}}"
value="{% if global_config[setting]['value'] %}
{{global_config[setting]['value']}} {% else %} {{value['default']}} {% endif
%}" />
<svg
checkbox-handler="{{value['id']}}"
class="{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} pointer-events-none {% else %} cursor-pointer {% endif %} absolute fill-white dark:fill-gray-300 left-0 top-0 translate-x-1 translate-y-2 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"
></path>
</svg>
</div>
{% endif %}
<!-- end checkbox -->
<!-- invalid feedback -->
<div class="hidden text-sm dark:text-red-500">
{{value['label']}} is invalid and must match this pattern:
{{value['regex']|safe}}
</div>
<!--end invalid feedback-->
</div>
{% endif %} {% endfor %}
<!-- end plugin settings -->
</div>
<!-- end plugin unless multiple -->
<!-- plugin multiple-->
<div {{current_endpoint}}-settings class="w-full grid grid-cols-12">
{% for setting, value in plugin["settings"].items() %}{% if current_endpoint
== "global-config" and value['context'] == "global" and value['multiple'] or current_endpoint ==
"services" and value['context'] == "multisite" and value['multiple'] %}
<div
class=" mx-0 sm:mx-4 my-2 col-span-12 md:mx-6 md:my-3 md:col-span-6 2xl:mx-6 2xl:my-3 2xl:col-span-4"
id="form-edit-{{current_endpoint}}-{{ value["id"] }}">
<!-- title and info -->
<div class="flex items-center my-1 relative">
<h5
class="transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>
{{value["label"]}}
</h5>
<svg
{{current_endpoint}}-info-btn="{{ value["label"] }}"
class="cursor-pointer fill-blue-500 h-5 w-5 ml-2 hover:brightness-75"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"
/>
</svg>
<!-- popover -->
<div class="hidden transition z-50 rounded-md p-3 left-0 -translate-y-7 bottom-0 absolute bg-blue-500"
{{current_endpoint}}-info-popover="{{ value["label"] }}"
>
<p class="transition duration-300 ease-in-out dark:opacity-90 font-bold text-sm text-white m-0" >{{value['help']}}
</p>
</div>
<!-- end popover -->
</div>
<!-- end title and info -->
<!-- input -->
{% if value["type"] != "select" and value["type"] != "check" %}
<input
default-value="{{global_config[setting]['value']}}" default-method="{{global_config[setting]['method']}}"
{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} disabled {% endif %} id="{{setting}}" name="{{setting}}"
class="duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 md:py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500 disabled:bg-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 dark:disabled:text-gray-300 disabled:text-gray-700"
value="{% if global_config[setting]['value'] %} {{global_config[setting]['value']}} {% else %} {{value['default']}} {% endif %}" type="{{value['type']}}" pattern="{{value['regex']|safe}}" />
{% endif %}
<!-- end input -->
<!-- select -->
{% if value["type"] == "select" %}
<!-- default hidden-->
<select default-method="{{global_config[setting]['method']}}" default-value="{{value['default']}}"
id="{{setting}}" name="{{setting}}" {{current_endpoint}}-setting-select-default="{{value['id']}}" type="form-select" id="{{setting}}" name="{{setting}}"
class="hidden">
{% for item in value['select'] %}
<option value="{{item}}" {% if global_config[setting]['value'] and global_config[setting]['value'] == item or not global_config[setting]['value'] and value['default'] == item %} selected{% endif %}>{{item}}</option>
{% endfor %}
</select>
<!-- end default hidden-->
<!--custom-->
<div class="relative">
<button
{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} disabled {% endif %} {{current_endpoint}}-setting-select="{{value['id']}}"
default-value="{{global_config[setting]['value']}}"
type="button"
class="disabled:opacity-75 dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 focus:border-green-500 flex justify-between align-middle items-center text-left text-sm leading-5.6 ease w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 md:py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
>
{% for item in value['select'] %} {% if global_config[setting]['value'] and
global_config[setting]['value'] == item %}
<span
{{current_endpoint}}-setting-select-text="{{value['id']}}"
value="{{global_config[setting]['value']}}"
>{{global_config[setting]['value']}}</span
>
{% elif not global_config[setting]['value'] and value['default'] == item %}
<span
{{current_endpoint}}-setting-select-text="{{value['id']}}"
value="{{value['default']}}"
>{{value['default']}}</span
>
{% endif %} {% endfor %}
<!-- chevron -->
<svg
{{current_endpoint}}-setting-select="{{value['id']}}"
class="transition-transform h-4 w-4 fill-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
<!-- end chevron -->
</button>
<!-- dropdown-->
<div
{{current_endpoint}}-setting-select-dropdown="{{value['id']}}"
class="hidden z-100 absolute h-full flex-col w-full mt-2"
>
{% for item in value['select'] %} {% if global_config[setting]['value'] and
global_config[setting]['value'] == item or not global_config[setting]['value']
and value['default'] == item %}
<button
type="button"
value="{{item}}"
{{current_endpoint}}-setting-select-dropdown-btn="{{value['id']}}"
type="button"
class="{% if loop.index == 1 %} border-t rounded-t {% endif %} {% if loop.index == loop.length %}rounded-b {% endif %} border-b border-l border-r border-gray-300 hover:brightness-90 bg-primary text-white my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
{{item}}
</button>
{% else %}
<button
type="button"
value="{{item}}"
{{current_endpoint}}-setting-select-dropdown-btn="{{value['id']}}"
type="button"
class="{% if loop.index == 1 %} border-t rounded-t {% endif %} {% if loop.index == loop.length %}rounded-b {% endif %} border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
{{item}}
</button>
{% endif %} {% endfor %}
</div>
<!-- end dropdown-->
</div>
<!-- end custom-->
{% endif %}
<!-- checkbox -->
{% if value["type"] == "check" %}
<div checkbox-handler="{{value['id']}}" class="relative mb-7 md:mb-0">
<input id="{{setting}}" name="{{setting}}"
default-method="{{global_config[setting]['method']}}"
default-value="{{global_config[setting]['value']}}" {% if
global_config[setting]['method'] != 'ui' or global_config[setting]['method']
!= 'default' %} disabled {% endif %} {% if global_config[setting]['value'] and
global_config[setting]['value'] == 'yes' or not
global_config[setting]['value'] and value['default'] == 'yes' %} checked {%
endif %} id="checkbox-{{value['id']}}" class="relative {% if
global_config[setting]['method'] != 'ui' and global_config[setting]['method']
!= 'default' %} pointer-events-none {% else %} cursor-pointer {% endif %}
dark:border-slate-600 dark:bg-slate-700 z-10 checked:z-0 w-5 h-5 ease
text-base rounded-1.4 checked:bg-primary checked:border-primary
dark:checked:bg-primary dark:checked:border-primary duration-250 float-left
mt-1 appearance-none border border-gray-300 bg-white bg-contain bg-center
bg-no-repeat align-top transition-all disabled:bg-gray-400
disabled:border-gray-400 dark:disabled:bg-gray-800
dark:disabled:border-gray-800 disabled:text-gray-700
dark:disabled:text-gray-300" type="checkbox" pattern="{{value['regex']|safe}}"
value="{% if global_config[setting]['value'] %}
{{global_config[setting]['value']}} {% else %} {{value['default']}} {% endif
%}" />
<svg
checkbox-handler="{{value['id']}}"
class="{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} pointer-events-none {% else %} cursor-pointer {% endif %} absolute fill-white dark:fill-gray-300 left-0 top-0 translate-x-1 translate-y-2 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"
></path>
</svg>
</div>
{% endif %}
<!-- end checkbox -->
<!-- invalid feedback -->
<div class="hidden text-sm dark:text-red-500">
{{value['label']}} is invalid and must match this pattern:
{{value['regex']|safe}}
</div>
<!--end invalid feedback-->
</div>
{% endif %} {% endfor %}
<!-- end plugin settings -->
</div>
<!-- end plugin multiple-->
</div>
<!-- end dropdown actions -->
{% endfor %}
<!-- end plugin item -->

View File

@ -0,0 +1,163 @@
{% set current_endpoint = url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-')
%}
{% set global_config =
config["CONFIG"].get_config() %}
{% set plugins = config["CONFIG"].get_plugins() %}
<!-- plugin item -->
{% for plugin in plugins %}
<div {{current_endpoint}}-item="{{plugin['id']}}" id="{{plugin['id']}}" class="hidden w-full">
<!-- title and desc -->
<div class="col-span-12">
<h5 class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0">{{plugin['name']}} <span>{{plugin['version']}}</span></h5>
<div class="transition duration-300 ease-in-out dark:opacity-90 ml-2 text-sm mb-2 dark:text-gray-400">
{{plugin['description']}}
</div>
</div>
<!-- end title and desc -->
<!-- plugin settings not multiple -->
<div
{{current_endpoint}}-settings
class="w-full grid grid-cols-12"
>
{% for setting, value in plugin["settings"].items() %}{% if current_endpoint == "global-config" and
value['context'] == "global" or current_endpoint == "services" and value['context'] == "multisite" %}
<div
{%if value['multiple'] %}multiple="{{value['multiple']}}"{% endif %}
class=" {%if value['multiple'] %}hidden{% endif %}
mx-0 sm:mx-4 my-2 col-span-12 md:mx-6 md:my-3 md:col-span-6 2xl:mx-6 2xl:my-3 2xl:col-span-4"
id="form-edit-{{current_endpoint}}-{{ value["id"] }}"
>
<!-- title and info -->
<div class="flex items-center my-1 relative">
<h5 class="transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300">{{value["label"]}}</h5>
<svg
{{current_endpoint}}-info-btn="{{ value["label"] }}"
class="cursor-pointer fill-blue-500 h-5 w-5 ml-2 hover:brightness-75"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"
/>
</svg>
<!-- popover -->
<div class="hidden transition z-50 rounded-md p-3 left-0 -translate-y-7 bottom-0 absolute bg-blue-500"
{{current_endpoint}}-info-popover="{{ value["label"] }}"
>
<p class="transition duration-300 ease-in-out dark:opacity-90 font-bold text-sm text-white m-0" >{{value['help']}}
</p>
</div>
<!-- end popover -->
</div>
<!-- end title and info -->
<!-- input -->
{% if value["type"] != "select" and value["type"] != "check" %}
<input
default-value="{{global_config[setting]['value']}}" default-method="{{global_config[setting]['method']}}"
{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} disabled {% endif %} id="{{setting}}" name="{{setting}}"
class="duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 md:py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500 disabled:bg-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 dark:disabled:text-gray-300 disabled:text-gray-700"
value="{% if global_config[setting]['value'] %} {{global_config[setting]['value']}} {% else %} {{value['default']}} {% endif %}" type="{{value['type']}}" pattern="{{value['regex']|safe}}" />
{% endif %}
<!-- end input -->
<!-- select -->
{% if value["type"] == "select" %}
<!-- default hidden-->
<select default-method="{{global_config[setting]['method']}}" default-value="{{value['default']}}"
id="{{setting}}" name="{{setting}}" {{current_endpoint}}-setting-select-default="{{value['id']}}" type="form-select" id="{{setting}}" name="{{setting}}"
class="hidden">
{% for item in value['select'] %}
<option value="{{item}}" {% if global_config[setting]['value'] and global_config[setting]['value'] == item or not global_config[setting]['value'] and value['default'] == item %} selected{% endif %}>{{item}}</option>
{% endfor %}
</select>
<!-- end default hidden-->
<!--custom-->
<div class="relative">
<button default-value="{{global_config[setting]['value']}}"
{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} disabled {% endif %} {{current_endpoint}}-setting-select="{{value['id']}}"
type="button"
class="disabled:opacity-75 dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 focus:border-green-500 flex justify-between align-middle items-center text-left text-sm leading-5.6 ease w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 md:py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
>
{% for item in value['select'] %}
{% if global_config[setting]['value'] and global_config[setting]['value'] == item %}
<span {{current_endpoint}}-setting-select-text="{{value['id']}}"
value="{{global_config[setting]['value']}}">{{global_config[setting]['value']}}</span>
{% elif not global_config[setting]['value'] and value['default'] == item %}
<span {{current_endpoint}}-setting-select-text="{{value['id']}}"
value="{{value['default']}}">{{value['default']}}</span>
{% endif %}
{% endfor %}
<!-- chevron -->
<svg
{{current_endpoint}}-setting-select="{{value['id']}}"
class="transition-transform h-4 w-4 fill-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
<!-- end chevron -->
</button>
<!-- dropdown-->
<div
{{current_endpoint}}-setting-select-dropdown="{{value['id']}}"
class="hidden z-100 absolute h-full flex-col w-full mt-2 "
>
{% for item in value['select'] %}
{% if global_config[setting]['value'] and global_config[setting]['value'] == item or not global_config[setting]['value'] and value['default'] == item %}
<button type="button" value="{{item}}" {{current_endpoint}}-setting-select-dropdown-btn="{{value['id']}}" type="button" class="{% if loop.index == 1 %} border-t rounded-t {% endif %} {% if loop.index == loop.length %}rounded-b {% endif %} border-b border-l border-r border-gray-300 hover:brightness-90 bg-primary text-white my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>{{item}}</button>
{% else %}
<button type="button" value="{{item}}" {{current_endpoint}}-setting-select-dropdown-btn="{{value['id']}}" type="button" class="{% if loop.index == 1 %} border-t rounded-t {% endif %} {% if loop.index == loop.length %}rounded-b {% endif %} border-b border-l border-r border-gray-300 hover:bg-gray-100 bg-white text-gray-700 my-0 relative px-6 py-2 text-center align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>{{item}}</button>
{% endif %}
{% endfor %}
</div>
<!-- end dropdown-->
</div>
<!-- end custom-->
{% endif %}
<!-- checkbox -->
{% if value["type"] == "check" %}
<div checkbox-handler="{{value['id']}}" class="relative mb-7 md:mb-0">
<input id="{{setting}}" name="{{setting}}"
default-method="{{global_config[setting]['method']}}" default-value="{{global_config[setting]['value']}}"
{% if global_config[setting]['method'] != 'ui' or global_config[setting]['method'] != 'default' %} disabled {% endif %}
{% if global_config[setting]['value'] and global_config[setting]['value'] == 'yes' or not global_config[setting]['value'] and value['default'] == 'yes' %} checked {% endif %} id="checkbox-{{value['id']}}"
class="relative {% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} pointer-events-none {% else %} cursor-pointer {% endif %} dark:border-slate-600 dark:bg-slate-700 z-10 checked:z-0 w-5 h-5 ease text-base rounded-1.4 checked:bg-primary checked:border-primary dark:checked:bg-primary dark:checked:border-primary duration-250 float-left mt-1 appearance-none border border-gray-300 bg-white bg-contain bg-center bg-no-repeat align-top transition-all disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 disabled:text-gray-700 dark:disabled:text-gray-300"
type="checkbox"
pattern="{{value['regex']|safe}}"
value="{% if global_config[setting]['value'] %} {{global_config[setting]['value']}} {% else %} {{value['default']}} {% endif %}"
/>
<svg checkbox-handler="{{value['id']}}" class="{% if global_config[setting]['method'] != 'ui' and global_config[setting]['method'] != 'default' %} pointer-events-none {% else %} cursor-pointer {% endif %} absolute fill-white dark:fill-gray-300 left-0 top-0 translate-x-1 translate-y-2 h-3 w-3 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"></path>
</svg>
</div>
{% endif %}
<!-- end checkbox -->
<!-- invalid feedback -->
<div class="hidden text-sm dark:text-red-500">
{{value['label']}} is invalid and must match this pattern: {{value['regex']|safe}}
</div>
<!--end invalid feedback -->
</div>
{% endif %}
{% endfor %}
<!-- end plugin settings -->
</div>
<!-- end plugin settings not multiple -->
</div>
{% endfor %}
<!-- end plugin item -->

View File

@ -1,36 +1,59 @@
{% extends "base.html" %} {% block content %} {% set global_config =
config["CONFIG"].get_config() %}
<!-- tabs -->
{% include "settings_tabs.html" %}
<!-- end tabs-->
<!-- form global conf -->
<form
global-config-form
id="form-edit-global-configs"
method="POST"
class="flex flex-col justify-between overflow-hidden overflow-y-auto max-h-135 md:max-h-160 dark:brightness-110 col-span-12 break-words bg-white shadow-xl p-4 dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
<div class="p-4 col-span-12 relative flex flex-col min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="flex justify-start items-center gap-x-4 gap-y-2 mb-2">
<h5 class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0">CONFIGS</h5>
<!-- search inpt-->
<div class="flex relative col-span-12 sm:col-span-6 lg:col-span-4 3xl:col-span-3">
<!-- general container -->
{% include "settings_general.html" %}
<!-- end general container -->
<!-- plugin item -->
{% include "settings_plugins.html" %}
<!-- end plugin item -->
<!-- submit -->
<div class="flex w-full justify-center mt-8 mb-2">
<button
type="submit"
class="tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal text-md ease-in shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
>
SAVE
</button>
<input
type="text"
id="settings-filter"
name="settings-filter"
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-3 py-1 font-normal text-gray-700 transition-all placeholder:text-gray-500"
placeholder="key words"
pattern="(.*?)"
required
/>
</div>
<!-- end search inpt-->
</div>
<!-- end submit -->
</form>
<!--end form global conf -->
<!-- tabs -->
{% include "settings_tabs.html" %}
<!-- end tabs-->
</div>
{% endblock %}
<!-- form global conf -->
<form
global-config-form
id="form-edit-global-configs"
method="POST"
class="flex flex-col justify-between overflow-hidden overflow-y-auto max-h-135 md:max-h-160 dark:brightness-110 col-span-12 break-words bg-white shadow-xl p-4 dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<!-- general container -->
{% include "settings_general.html" %}
<!-- end general container -->
<!-- plugin item -->
{% include "settings_plugins.html" %}
<!-- end plugin item -->
<!-- submit -->
<div class="flex w-full justify-center mt-8 mb-2">
<button
type="submit"
class="tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal text-md ease-in shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
>
SAVE
</button>
</div>
<!-- end submit -->
</form>
<!--end form global conf -->
{% endblock %}
</div>

View File

@ -18,7 +18,7 @@
<!-- tailwind style -->
<link rel="stylesheet" type="text/css" href="./css/dashboard.css" />
<script type="module" defer src="./js/dashboard.js"></script>
<script type="module" defer src="./js/global.js"></script>
<script
type="text/javascript"

View File

@ -14,14 +14,14 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{jobs_total}}
{{jobs|length}}
</p>
</div>
<div class="flex items-center my-4">
<p
class="transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500 dark:opacity-80"
>
JOBS ERROS
JOBS ERRORS
</p>
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
@ -29,18 +29,6 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
{{jobs_errors}}
</p>
</div>
<div class="flex items-center my-4">
<p
class="transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500 dark:opacity-80"
>
AUTO UPDATE
</p>
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
2 mins
</p>
</div>
</div>
<!-- end info -->

View File

@ -122,6 +122,39 @@ url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %}
/>
</div>
<!-- end refresh delay input -->
<!-- refresh inp -->
<div
class="flex flex-col relative col-span-12 sm:col-span-6 2xl:col-span-4 3xl:col-span-3"
>
<h5
class="my-1 transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>
Update Delay
</h5>
<div checkbox-handler="live-update" class="relative mb-7 md:mb-0">
<input
id="live-update"
name="live-update"
default-method="default"
default-value="no"
class="z-0 relative cursor-pointer dark:border-slate-600 dark:bg-slate-700 z-10 checked:z-0 w-5 h-5 ease text-base rounded-1.4 checked:bg-primary checked:border-primary dark:checked:bg-primary dark:checked:border-primary duration-250 float-left mt-1 appearance-none border border-gray-300 bg-white bg-contain bg-center bg-no-repeat align-top transition-all disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 disabled:text-gray-700 dark:disabled:text-gray-300"
type="checkbox"
pattern="^(yes|no)$"
value="no"
/>
<svg
checkbox-handler="live-update"
class="cursor-pointer absolute fill-white dark:fill-gray-300 left-0 top-0 translate-x-1 translate-y-2 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"
></path>
</svg>
</div>
</div>
<!-- end refresh inp-->
<div class="col-span-12 w-full justify-center flex mt-2">
<button

View File

@ -301,8 +301,7 @@
<!-- end default anchor -->
<!-- plugin list -->
{% set plugins = config["CONFIG"].get_plugins() %} {% if
plugins_pages_count != 0 %}
{% set plugins = config["CONFIG"].get_plugins() %}
<div>
<ul>
<li class="w-full mt-4">
@ -342,7 +341,6 @@
</ul>
<!-- end plugin list -->
</div>
{% endif %}
</div>
<!-- end list items -->
</div>

View File

@ -1,6 +1,5 @@
{% extends "base.html" %} {% block content %}{% set current_endpoint =
url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %} {% set
plugins = config["CONFIG"].get_plugins() %}
url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %}
<!-- info -->
<div
class="p-4 col-span-12 md:col-span-5 2xl:col-span-4 relative min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
@ -15,7 +14,7 @@ plugins = config["CONFIG"].get_plugins() %}
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{plugins_total}}
{{plugins|length}}
</p>
</div>
<div class="flex items-center my-4">

View File

@ -26,6 +26,28 @@
</svg>
</button>
</div>
<div class="flex justify-start items-center gap-x-4 gap-y-2 mb-2">
<h5
class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0"
>
CONFIGS
</h5>
<!-- search inpt-->
<div
class="flex relative col-span-12 sm:col-span-6 lg:col-span-4 3xl:col-span-3"
>
<input
type="text"
id="settings-filter"
name="settings-filter"
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-3 py-1 font-normal text-gray-700 transition-all placeholder:text-gray-500"
placeholder="key words"
pattern="(.*?)"
required
/>
</div>
<!-- end search inpt-->
</div>
{% include "settings_tabs.html" %}
<!-- new and edit form -->
<form

View File

@ -336,7 +336,7 @@
<button
services-action="edit"
type="button"
service-name="{{service["SERVER_NAME"]['value']}}"
services-name="{{service["SERVER_NAME"]['value']}}"
class="dark:brightness-90 z-20 mx-1 bg-yellow-500 hover:bg-yellow-500/80 focus:bg-yellow-500/80 inline-block p-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal text-xs ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
>
@ -354,7 +354,7 @@
<button
services-action="delete"
type="button"
service-name="{{service["SERVER_NAME"]['value']}}"
services-name="{{service["SERVER_NAME"]['value']}}"
class="dark:brightness-90 z-20 mx-1 bg-red-500 hover:bg-red-500/80 focus:bg-red-500/80 inline-block p-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal text-xs ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
>
<svg

View File

@ -6,20 +6,21 @@
<!-- general container -->
<div {{current_endpoint}}-item="general" class="w-full">
<!-- general conf -->
<div class="col-span-12">
<div class="col-span-12" setting-header>
<h5 class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0">GENERAL</h5>
<div class="transition duration-300 ease-in-out dark:opacity-90 ml-2 text-sm mb-2 dark:text-gray-400">
General config : HTTP, DNS, LOG, API...
</div>
</div>
<div
{{current_endpoint}}-settings
class="w-full grid grid-cols-12"
id="edit-{{current_endpoint}}-general"
>
{% for
setting, value in config["CONFIG"].get_settings().items() %} {%
if current_endpoint == "global-config" and value["context"] == "global" and "label" in value %}
<div
<div setting-container
class="mx-0 sm:mx-4 my-2 col-span-12 md:mx-6 md:my-3 md:col-span-6 2xl:col-span-4 xl:mx-4 2xl:my-2 3xl:col-span-3"
id="form-edit-{{current_endpoint}}-{{ value["id"] }}"
>

View File

@ -12,7 +12,7 @@
class="hidden w-full"
>
<!-- title and desc -->
<div class="col-span-12">
<div class="col-span-12" setting-header>
<h5
class="transition duration-300 ease-in-out dark:opacity-90 ml-2 font-bold text-md uppercase dark:text-white mb-0"
>
@ -30,7 +30,7 @@
{% for setting, value in plugin["settings"].items() %}{% if current_endpoint
== "global-config" and value['context'] == "global" and not value['multiple'] or current_endpoint ==
"services" and value['context'] == "multisite" and not value['multiple'] %}
<div
<div setting-container
class="
mx-0 sm:mx-4 my-2 col-span-12 md:mx-6 md:my-3 md:col-span-6 2xl:mx-6 2xl:my-3 2xl:col-span-4"
id="form-edit-{{current_endpoint}}-{{ value["id"] }}">
@ -208,7 +208,7 @@
<!-- plugin multiple -->
{% if not plugin['multiple'] %}
<div class="flex items-center mx-0 sm:mx-4 md:mx-6 md:my-3 my-2 2xl:mx-6 2xl:my-3 col-span-12 ">
<div multiple-handler class="flex items-center mx-0 sm:mx-4 md:mx-6 md:my-3 my-2 2xl:mx-6 2xl:my-3 col-span-12 ">
<h5
class="transition duration-300 ease-in-out dark:opacity-90 text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>

View File

@ -9,7 +9,7 @@
<button
{{current_endpoint}}-item-handler="general"
type="button"
class=" border-primary dark:hover:bg-slate-800 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 border my-1 relative inline-block px-3 py-3 font-bold text-center uppercase align-middle transition-all rounded-none cursor-pointer bg-white hover:bg-gray-100 leading-normal text-sm ease-in tracking-tight-rem shadow-xs hover:shadow-md"
class=" brightness-75 border-primary dark:hover:bg-slate-800 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 border my-1 relative inline-block px-3 py-3 font-bold text-center uppercase align-middle transition-all rounded-none cursor-pointer bg-white hover:bg-gray-100 leading-normal text-sm ease-in tracking-tight-rem shadow-xs hover:shadow-md"
>
<div class="w-full flex justify-between items-center">
<!-- text and icon -->
@ -47,7 +47,7 @@
<button
{{current_endpoint}}-item-handler="{{ plugin['id'] }}"
type="button"
class=" border-primary dark:hover:bg-slate-800 dark:border-slate-600 dark:bg-slate-700 border my-1 relative inline-block px-3 py-3 font-bold text-center uppercase align-middle transition-all rounded-none cursor-pointer bg-white hover:bg-gray-100 leading-normal text-sm ease-in tracking-tight-rem shadow-xs hover:shadow-md"
class="{% if current_endpoint == 'service' and loop.first %} brightness-75 {% endif %} border-primary dark:hover:bg-slate-800 dark:border-slate-600 dark:bg-slate-700 border my-1 relative inline-block px-3 py-3 font-bold text-center uppercase align-middle transition-all rounded-none cursor-pointer bg-white hover:bg-gray-100 leading-normal text-sm ease-in tracking-tight-rem shadow-xs hover:shadow-md"
>
<div clas="w-full flex justify-between items-center">
<!-- text and icon -->

View File

@ -1,6 +1,5 @@
from datetime import datetime
from typing import List
from bs4 import Tag
import magic
import os
@ -91,6 +90,8 @@ def path_to_dict(
):
with open(path, "rb") as f:
d["content"] = f.read().decode("utf-8")
else:
d["content"] = "Download file to view content"
else:
config_types = [
"http",