mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Run pre-commit-config and apply it + update it
This commit is contained in:
parent
c73e9bf161
commit
b2f9fab7ad
36 changed files with 147 additions and 2516 deletions
243
.github/workflows/1.5.yml
vendored
243
.github/workflows/1.5.yml
vendored
|
|
@ -2,249 +2,6 @@ name: Automatic tests (1.5)
|
|||
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [1.5]
|
||||
|
||||
jobs:
|
||||
# Containers
|
||||
build-containers:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [bunkerweb, scheduler, autoconf, ui]
|
||||
include:
|
||||
- image: bunkerweb
|
||||
dockerfile: src/bw/Dockerfile
|
||||
- image: scheduler
|
||||
dockerfile: src/scheduler/Dockerfile
|
||||
- image: autoconf
|
||||
dockerfile: src/autoconf/Dockerfile
|
||||
- image: ui
|
||||
dockerfile: src/ui/Dockerfile
|
||||
uses: ./.github/workflows/container-build.yml
|
||||
with:
|
||||
RELEASE: 1.5
|
||||
ARCH: linux/amd64
|
||||
CACHE: true
|
||||
IMAGE: ${{ matrix.image }}
|
||||
DOCKERFILE: ${{ matrix.dockerfile }}
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
# Build Linux packages
|
||||
build-packages:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
linux: [ubuntu, debian, fedora, rhel, rhel9, ubuntu-jammy]
|
||||
include:
|
||||
- linux: ubuntu
|
||||
package: deb
|
||||
- linux: ubuntu-jammy
|
||||
package: deb
|
||||
- linux: debian
|
||||
package: deb
|
||||
- linux: fedora
|
||||
package: rpm
|
||||
- linux: rhel
|
||||
package: rpm
|
||||
- linux: rhel9
|
||||
package: rpm
|
||||
uses: ./.github/workflows/linux-build.yml
|
||||
with:
|
||||
RELEASE: 1.5
|
||||
LINUX: ${{ matrix.linux }}
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
TEST: true
|
||||
PLATFORMS: linux/amd64
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
# UI tests
|
||||
prepare-tests-ui:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
- id: set-matrix
|
||||
run: |
|
||||
tests=$(find ./tests/ui/ -name "*_page.py" -type f -printf "%f\n" | jq -c --raw-input --slurp 'split("\n")| .[0:-1]')
|
||||
echo "tests=$tests" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
tests: ${{ steps.set-matrix.outputs.tests }}
|
||||
tests-ui:
|
||||
needs: [prepare-tests-ui, build-containers]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test: ${{ fromJson(needs.prepare-tests-ui.outputs.tests) }}
|
||||
uses: ./.github/workflows/tests-ui.yml
|
||||
with:
|
||||
TEST: ${{ matrix.test }}
|
||||
RELEASE: 1.5
|
||||
tests-ui-linux:
|
||||
needs: [prepare-tests-ui, build-packages]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test: ${{ fromJson(needs.prepare-tests-ui.outputs.tests) }}
|
||||
uses: ./.github/workflows/tests-ui-linux.yml
|
||||
with:
|
||||
TEST: ${{ matrix.test }}
|
||||
RELEASE: 1.5
|
||||
|
||||
# Core tests
|
||||
prepare-tests-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
- id: set-matrix
|
||||
run: |
|
||||
tests=$(find ./tests/core/ -maxdepth 1 -mindepth 1 -type d -printf "%f\n" | jq -c --raw-input --slurp 'split("\n")| .[0:-1]')
|
||||
echo "tests=$tests" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
tests: ${{ steps.set-matrix.outputs.tests }}
|
||||
tests-core:
|
||||
needs: [build-containers, prepare-tests-core]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
|
||||
uses: ./.github/workflows/test-core.yml
|
||||
with:
|
||||
TEST: ${{ matrix.test }}
|
||||
RELEASE: 1.5
|
||||
tests-core-linux:
|
||||
needs: [build-packages, prepare-tests-core]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
|
||||
uses: ./.github/workflows/test-core-linux.yml
|
||||
with:
|
||||
TEST: ${{ matrix.test }}
|
||||
RELEASE: 1.5
|
||||
secrets: inherit
|
||||
|
||||
# Push with 1.5 tag
|
||||
push-1_5:
|
||||
needs: [tests-ui, tests-core]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Push BW image
|
||||
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
|
||||
env:
|
||||
FROM: "bunkerweb"
|
||||
TO: "bunkerweb"
|
||||
- name: Push scheduler image
|
||||
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
|
||||
env:
|
||||
FROM: "scheduler"
|
||||
TO: "bunkerweb-scheduler"
|
||||
- name: Push UI image
|
||||
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
|
||||
env:
|
||||
FROM: "ui"
|
||||
TO: "bunkerweb-ui"
|
||||
- name: Push autoconf image
|
||||
run: docker pull ghcr.io/bunkerity/$FROM-tests:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 bunkerity/$TO:1.5 && docker tag ghcr.io/bunkerity/$FROM-tests:1.5 ghcr.io/bunkerity/$TO:1.5 && docker push bunkerity/$TO:1.5 && docker push ghcr.io/bunkerity/$TO:1.5
|
||||
env:
|
||||
FROM: "autoconf"
|
||||
TO: "bunkerweb-autoconf"
|
||||
|
||||
# Push Linux packages
|
||||
push-packages:
|
||||
needs: [tests-ui-linux, tests-core-linux]
|
||||
strategy:
|
||||
matrix:
|
||||
linux: [ubuntu, debian, fedora, el, el9, ubuntu-jammy]
|
||||
arch: [amd64]
|
||||
include:
|
||||
- release: 1.5
|
||||
repo: bunkerweb
|
||||
- linux: ubuntu
|
||||
package_arch: amd64
|
||||
separator: _
|
||||
suffix: ""
|
||||
version: noble
|
||||
package: deb
|
||||
- linux: debian
|
||||
package_arch: amd64
|
||||
separator: _
|
||||
suffix: ""
|
||||
version: bookworm
|
||||
package: deb
|
||||
- linux: fedora
|
||||
package_arch: x86_64
|
||||
separator: "-"
|
||||
suffix: "1."
|
||||
version: 40
|
||||
package: rpm
|
||||
- linux: el
|
||||
package_arch: x86_64
|
||||
separator: "-"
|
||||
suffix: "1."
|
||||
version: 8
|
||||
package: rpm
|
||||
- linux: el9
|
||||
package_arch: x86_64
|
||||
separator: "-"
|
||||
suffix: "1."
|
||||
version: 9
|
||||
package: rpm
|
||||
- linux: ubuntu-jammy
|
||||
package_arch: amd64
|
||||
separator: _
|
||||
suffix: ""
|
||||
version: jammy
|
||||
package: deb
|
||||
uses: ./.github/workflows/push-packagecloud.yml
|
||||
with:
|
||||
SEPARATOR: ${{ matrix.separator }}
|
||||
SUFFIX: ${{ matrix.suffix }}
|
||||
REPO: ${{ matrix.repo }}
|
||||
LINUX: ${{ matrix.linux }}
|
||||
VERSION: ${{ matrix.version }}
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
BW_VERSION: ${{ matrix.release }}
|
||||
PACKAGE_ARCH: ${{ matrix.package_arch }}
|
||||
ARCH: ${{ matrix.arch }}
|
||||
secrets:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
name: Automatic tests (1.5)
|
||||
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [1.5]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
exclude: (^src/ui/client|^LICENSE.md$|^src/VERSION$|^env/|^src/(bw/misc/root-ca.pem$|deps/src/|common/core/modsecurity/files|ui/static/(js/(editor/|utils/purify/|tsparticles\.bundle\.min\.js)|css/dashboard\.css))|\.(svg|drawio|patch\d?|ascii|tf|tftpl|key)$)
|
||||
exclude: (^src/ui/client|^LICENSE.md$|^src/VERSION$|^env/|^examples/community/|^src/(bw/misc/root-ca.pem$|deps/src/|common/core/modsecurity/files|ui/static/(js/(editor/|utils/purify/|tsparticles\.bundle\.min\.js)|css/dashboard\.css))|\.(svg|drawio|patch\d?|ascii|tf|tftpl|key)$)
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0
|
||||
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
|
||||
hooks:
|
||||
- id: requirements-txt-fixer
|
||||
name: Fix requirements.txt and requirements.in files
|
||||
|
|
@ -17,7 +17,7 @@ repos:
|
|||
- id: check-case-conflict
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 3702ba224ecffbcec30af640c149f231d90aebdb # frozen: 24.4.2
|
||||
rev: b965c2a5026f8ba399283ba3e01898b012853c79 # frozen: 24.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
name: Black Python Formatter
|
||||
|
|
@ -43,7 +43,7 @@ repos:
|
|||
args: ["--std", "min", "--codes", "--ranges", "--no-cache"]
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7d37d9032d0d161634be4554273c30efd4dea0b3 # frozen: 7.0.0
|
||||
rev: e43806be3607110919eff72939fda031776e885a # frozen: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
name: Flake8 Python Linter
|
||||
|
|
@ -62,12 +62,12 @@ repos:
|
|||
- id: codespell
|
||||
name: Codespell Spell Checker
|
||||
exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*|src/ui/app/static/(fonts|libs)/.+)$
|
||||
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py,src/ui/app/static/json/countries.geojson,src/ui/app/static/js/pages/reports.js
|
||||
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py,src/ui/app/static/json/countries.geojson,src/ui/app/static/js/pages/reports.js,src/ui/app/static/json/periscop.min.json,src/ui/app/static/json/blockhaus.min.json
|
||||
language: python
|
||||
types: [text]
|
||||
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: 145400593c178304246371bc45290588bc72f43e # frozen: v8.18.2
|
||||
rev: ce2702a4889da86abb07e0fb0482e9a6e12a9f24 # frozen: v8.20.0
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,97 +0,0 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Merge all components md on this file name
|
||||
const finalFile = "ui-components.md";
|
||||
|
||||
|
||||
// Format merge file
|
||||
function formatMd() {
|
||||
// Create a md file to merge
|
||||
const merge = path.join(finalFile);
|
||||
|
||||
// Get data from merge
|
||||
let data = fs.readFileSync(merge, "utf8");
|
||||
let isLevel = true;
|
||||
let currAttemps = 0;
|
||||
const maxAttemps = 6;
|
||||
while (isLevel && currAttemps < maxAttemps) {
|
||||
currAttemps++;
|
||||
const titles = [];
|
||||
let tag = "#";
|
||||
for (let i = 0; i < currAttemps; i++) {
|
||||
tag += "#";
|
||||
}
|
||||
tag += " ";
|
||||
|
||||
// Each time, get the first level title and add it to the titles array
|
||||
data.split("\n").forEach((line) => {
|
||||
if (line.startsWith(tag) && line.includes("/")) {
|
||||
const firstLevel = line.split("/")[0];
|
||||
if (!titles.includes(firstLevel.replace(tag, "").trim()))
|
||||
titles.push(firstLevel.replace(tag, ""));
|
||||
}
|
||||
});
|
||||
// Create a top title at the first occurrence
|
||||
// And remove from component the first level string
|
||||
titles.forEach((title) => {
|
||||
let isTitleSet = false;
|
||||
data.split("\n").forEach((line) => {
|
||||
// For title
|
||||
if (line.startsWith(tag) && line.includes("/")) {
|
||||
// Add a top title before the current line
|
||||
if (!isTitleSet && line.includes(`${title}/`)) {
|
||||
data = data.replace(
|
||||
line,
|
||||
`${tag} ${title}\n\n${line
|
||||
.replace(tag, "#" + tag)
|
||||
.replace(`${title}/`, "")}`,
|
||||
);
|
||||
isTitleSet = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.includes(`${title}/`)) {
|
||||
data = data.replace(
|
||||
line,
|
||||
line.replace(tag, "#" + tag).replace(`${title}/`, ""),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update the child of .vue component title to keep title levels consistency
|
||||
let componentTag = "";
|
||||
let dataSplit = data.split("\n");
|
||||
data.split("\n").forEach((line, id) => {
|
||||
if (line.startsWith("#") && line.includes(".vue")) {
|
||||
componentTag = line.split(" ")[0];
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(line.startsWith("#") && line.includes("Parameters")) ||
|
||||
(line.startsWith("#") && line.includes("Examples"))
|
||||
) {
|
||||
const elTag = line.split(" ")[0];
|
||||
// get line per id
|
||||
const updateLine = line.replace(elTag, `${componentTag}#`);
|
||||
dataSplit[id] = updateLine;
|
||||
}
|
||||
});
|
||||
// Update the data with split
|
||||
data = dataSplit.join("\n");
|
||||
|
||||
// Add title and description
|
||||
const title = "# UI Components";
|
||||
const description =
|
||||
"This page contains all the UI components used in the application.";
|
||||
data = `${title}\n\n${description}\n\n${data}`;
|
||||
|
||||
fs.writeFileSync(merge, data, "utf8");
|
||||
}
|
||||
|
||||
formatMd();
|
||||
209
docs/vue2md.py
209
docs/vue2md.py
|
|
@ -1,209 +0,0 @@
|
|||
from os.path import abspath
|
||||
from pathlib import Path
|
||||
from subprocess import Popen, PIPE
|
||||
from typing import List
|
||||
from shutil import rmtree
|
||||
from re import search
|
||||
from traceback import format_exc
|
||||
|
||||
outputFilename = "ui-components.md"
|
||||
# We want to get path of the folder where our components are
|
||||
# The path is "../src/client/dashboard/src/components" from here
|
||||
|
||||
inputFolder = abspath("../src/ui/client/dashboard/components")
|
||||
outputFolder = abspath("../docs/components")
|
||||
outputFile = abspath("../docs")
|
||||
components_path_to_exclude = ("components/Icons", "components/Forms/Error", "components/Dashboard", "components/Builder")
|
||||
|
||||
|
||||
def run_command(command: List[str]) -> int:
|
||||
"""Utils to run a subprocess command. This is useful to run npm commands to build vite project"""
|
||||
print(f"Running command: {command}", flush=True)
|
||||
try:
|
||||
process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, text=True)
|
||||
while process.poll() is None:
|
||||
if process.stdout is not None:
|
||||
for line in process.stdout:
|
||||
print(line.strip(), flush=True)
|
||||
|
||||
if process.returncode != 0:
|
||||
print("Error while running command", flush=True)
|
||||
print(process.stdout.read(), flush=True)
|
||||
print(process.stderr.read(), flush=True)
|
||||
return 1
|
||||
except BaseException as e:
|
||||
print(f"Error while running command: {e}", flush=True)
|
||||
return 1
|
||||
|
||||
print("Command executed successfully", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
def install_npm_packages():
|
||||
"""Install all packages needed to run the script"""
|
||||
# Install documentation package
|
||||
run_command("npm install -g documentation")
|
||||
|
||||
|
||||
def reset():
|
||||
"""Reset the docs folder"""
|
||||
# delete the output folder even if not empty
|
||||
rmtree(outputFolder, ignore_errors=True)
|
||||
# remove outputfilename
|
||||
output_file_path = Path(outputFile) / outputFilename
|
||||
output_file_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def vue2js():
|
||||
"""Get the script part of a Vue file and create a JS file"""
|
||||
# Create outputFolder if not exists
|
||||
Path(outputFolder).mkdir(parents=True, exist_ok=True)
|
||||
# Get every subfolders from the input folder
|
||||
for folder in Path(inputFolder).rglob("*"):
|
||||
# Get only vue file
|
||||
if not folder.is_file() or folder.suffix != ".vue":
|
||||
continue
|
||||
|
||||
# Exclude some files
|
||||
if any(folder_path in folder.as_posix() for folder_path in components_path_to_exclude):
|
||||
continue
|
||||
|
||||
# Read the file content
|
||||
data = folder.read_text()
|
||||
# Get only the content between <script setup> and </script> tag
|
||||
script = data.split("<script setup>")[1].split("</script>")[0]
|
||||
# Get index of jsdoc comments
|
||||
first_doc_index_start = script.find("/**")
|
||||
first_doc_index_end = script.find("*/")
|
||||
if first_doc_index_start != -1 and first_doc_index_end != -1:
|
||||
# get content before first_doc_index_end
|
||||
script = script[first_doc_index_start : first_doc_index_end + 2]
|
||||
|
||||
# Create a file on the output folder with the same name but with .js extension
|
||||
fileName = folder.name.replace(".vue", ".js")
|
||||
dest = Path(outputFolder) / fileName
|
||||
dest.write_text(script)
|
||||
|
||||
|
||||
def js2md():
|
||||
"""Run a command to render markdown from JS files"""
|
||||
# Get all files from the output folder
|
||||
files = list(Path(outputFolder).rglob("*"))
|
||||
process_list = []
|
||||
# Create a markdown file for each JS file
|
||||
for file in files:
|
||||
# Run a process `documentation build <filename> -f md > <filename>.md
|
||||
command = f"documentation build {file} -f md > {file.with_suffix('.md')}"
|
||||
# Run the command
|
||||
# I want to run this command async
|
||||
process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, text=True)
|
||||
process_list.append(process)
|
||||
|
||||
# Wait that all processes are done
|
||||
for process in process_list:
|
||||
process.wait()
|
||||
# Remove js files after
|
||||
for file in files:
|
||||
file.unlink()
|
||||
|
||||
|
||||
def formatMd():
|
||||
"""Format each markdown file to remove useless data and format some data like params"""
|
||||
# Get all files from the output folder
|
||||
files = list(Path(outputFolder).rglob("*"))
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
# Get the title from first line
|
||||
data = file.read_text()
|
||||
# Remove everything after a [1]: tag
|
||||
data = data.split("[1]:")[0]
|
||||
# Remove ### Table of contents
|
||||
data = data.replace("### Table of Contents", "")
|
||||
# Remove everything before the first ## tag
|
||||
if "## " in data:
|
||||
index = data.index("## ")
|
||||
data = data[index:]
|
||||
|
||||
# I want to loop on each line
|
||||
lines = data.split("\n")
|
||||
line_result = []
|
||||
for line in lines:
|
||||
# remove space (so   or  )
|
||||
line = line.replace(" ", "").replace(" ", "")
|
||||
|
||||
if line.startswith("#") and ".vue" in line and "\\.vue" in line:
|
||||
line = line.replace("\\.vue", ".vue")
|
||||
|
||||
# Case not a param, keep the line as is
|
||||
if not line.startswith("*"):
|
||||
line_result.append(line)
|
||||
continue
|
||||
|
||||
# get line without first char
|
||||
line = "-" + line[1:]
|
||||
|
||||
# remove each **[string][num]** pattern in a param by **string**
|
||||
reg = r"\[\w+\]\[\d+\]"
|
||||
while search(reg, line):
|
||||
# get data of the pattern
|
||||
pattern = search(reg, line).group()
|
||||
# get content of first bracket
|
||||
content = pattern.split("][")[0].replace("[", "")
|
||||
line = line.replace(pattern, f"{content}")
|
||||
|
||||
line_result.append(line)
|
||||
|
||||
# I can merge the lines
|
||||
data = "\n".join(line_result)
|
||||
# update the file with the new content
|
||||
file.write_text(data)
|
||||
except BaseException:
|
||||
print(format_exc(), flush=True)
|
||||
print("Error while parsing file", str(file.name), flush=True)
|
||||
exit(1)
|
||||
|
||||
|
||||
def mergeMd():
|
||||
"""Merge all markdown files into one"""
|
||||
# Get all files from the output folder
|
||||
files = list(Path(outputFolder).rglob("*"))
|
||||
# Create order using the tag title path of each file
|
||||
order = []
|
||||
for file in files:
|
||||
# Get the title from first line
|
||||
data = file.read_text()
|
||||
filePath = data.split("\n")[0].replace("## ", "")
|
||||
order.append({"path": filePath, "fileName": str(file.name)})
|
||||
|
||||
# Sort by path
|
||||
order.sort(key=lambda x: x["path"])
|
||||
|
||||
# Create the md file to merge
|
||||
merge = Path(outputFile) / outputFilename
|
||||
merge.write_text("")
|
||||
# Append each file in order and keep indentation
|
||||
for info in order:
|
||||
file_path = Path(outputFolder) / info["fileName"]
|
||||
data = file_path.read_text()
|
||||
merge.write_text(merge.read_text() + data)
|
||||
|
||||
# Remove all files
|
||||
rmtree(outputFolder, ignore_errors=True)
|
||||
|
||||
|
||||
def formatMergeMd():
|
||||
"""ATM didn't convert the js function to python.
|
||||
So I will run a command to format the file"""
|
||||
|
||||
command = "node ./vue2md.js"
|
||||
run_command(command)
|
||||
|
||||
|
||||
install_npm_packages()
|
||||
reset()
|
||||
vue2js()
|
||||
js2md()
|
||||
formatMd()
|
||||
mergeMd()
|
||||
formatMergeMd()
|
||||
308
docs/web-ui.md
308
docs/web-ui.md
|
|
@ -2229,311 +2229,3 @@ After a successful login/password combination, you will be prompted to enter you
|
|||
```shell
|
||||
systemctl reload bunkerweb
|
||||
```
|
||||
|
||||
## UI development
|
||||
|
||||
The web UI is moving from Flask to Vue.js using a builder approach. I will detail some steps and parts of the UI development process.
|
||||
|
||||
### Create a dashboard page
|
||||
|
||||
A dashboard page is a page that will have at build time a separate html and js file. Furthermore, the main.py file will need to be updated to include the new page.
|
||||
|
||||
**1. Create page folder**
|
||||
|
||||
For the example, we will create a page named `example`.
|
||||
|
||||
First, you need to go to the `src/ui/client/dashboard/pages` folder and copy an existing page folder like `home` folder and rename it to `example`.
|
||||
|
||||
Don't forget to rename the `Home.vue` file to `Example.vue`, and `home.js` to `example.js`.
|
||||
|
||||
**2. Update page folder files**
|
||||
|
||||
Open `example.js` file, you get something like this :
|
||||
|
||||
```js
|
||||
import { createApp } from "vue"; // Utils
|
||||
import { createPinia } from "pinia"; // Global Vue.js store
|
||||
import { getI18n } from "@utils/lang.js"; // Get i18n
|
||||
import Home from "./Home.vue"; // Vue page app
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
// We set the page as app, add pinia plugin, and we add the i18n module with wanted prefix key and mount the app
|
||||
// The i18n keys are in the src/ui/client/dashboard/lang folder
|
||||
createApp(Home)
|
||||
.use(pinia)
|
||||
.use(getI18n(["dashboard", "action", "inp", "icons", "home"]))
|
||||
.mount("#app");
|
||||
```
|
||||
|
||||
You need to update it like this :
|
||||
|
||||
```js
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import { getI18n } from "@utils/lang.js";
|
||||
import Example from "./Example.vue";
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
createApp(Example)
|
||||
.use(pinia)
|
||||
.use(getI18n(["dashboard", "action", "inp", "icons", "example"])) // get example prefix keys from lang folder
|
||||
.mount("#app");
|
||||
```
|
||||
|
||||
Open `Home.vue` file, you get something like this :
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { reactive, onBeforeMount, onMounted } from "vue"; // built-in vue functions
|
||||
import DashboardLayout from "@components/Dashboard/Layout.vue"; // Dashboard base layout
|
||||
// BuilderHome is where we define the component to use for the current page
|
||||
// We will update it later
|
||||
import BuilderHome from "@components/Builder/Home.vue";
|
||||
// Global is utils for buttons or link actions
|
||||
import { useGlobal } from "@utils/global";
|
||||
|
||||
|
||||
// This is JSDOC
|
||||
/**
|
||||
* @name Page/Home.vue
|
||||
* @description This component is the home page.
|
||||
This page displays an overview of multiple stats related to BunkerWeb.
|
||||
*/
|
||||
|
||||
// Get data for builder logic
|
||||
const home = reactive({
|
||||
builder: "",
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
// Get builder data
|
||||
const dataAtt = "data-server-builder";
|
||||
const dataEl = document.querySelector(`[${dataAtt}]`);
|
||||
const data =
|
||||
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
|
||||
? JSON.parse(atob(dataEl.getAttribute(dataAtt)))
|
||||
: {};
|
||||
home.builder = data;
|
||||
});
|
||||
|
||||
// Use utils when app is built
|
||||
onMounted(() => {
|
||||
useGlobal();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Template part -->
|
||||
<DashboardLayout>
|
||||
<BuilderHome v-if="home.builder" :builder="home.builder" />
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
You need to update it like this :
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { reactive, onBeforeMount, onMounted } from "vue";
|
||||
import DashboardLayout from "@components/Dashboard/Layout.vue";
|
||||
// We will create it, or we can use
|
||||
// import BuilderCollection from "@components/Builder/Collection.vue";
|
||||
// to get all components but performance will be impacted
|
||||
import BuilderExample from "@components/Builder/Example.vue";
|
||||
import { useGlobal } from "@utils/global";
|
||||
|
||||
/**
|
||||
* @name Page/Example.vue
|
||||
* @description This component is the Example page.
|
||||
*/
|
||||
|
||||
// Get data for builder logic
|
||||
const example = reactive({
|
||||
builder: "",
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
// Get builder data
|
||||
const dataAtt = "data-server-builder";
|
||||
const dataEl = document.querySelector(`[${dataAtt}]`);
|
||||
const data =
|
||||
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
|
||||
? JSON.parse(atob(dataEl.getAttribute(dataAtt)))
|
||||
: {};
|
||||
example.builder = data;
|
||||
});
|
||||
|
||||
// Use utils when app is built
|
||||
onMounted(() => {
|
||||
useGlobal();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Template part -->
|
||||
<DashboardLayout>
|
||||
<BuilderExample v-if="example.builder" :builder="example.builder" />
|
||||
<!-- alternative -->
|
||||
<!-- <BuilderCollection v-if="example.builder" :builder="example.builder" /> -->
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
Open `index.html` file, you get something like this :
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="/css/flag-icons.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BunkerWeb | Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
class="hidden"
|
||||
data-server-global='{"username" : "admin", "plugins_page": [{"id" : "antibot", "name": "Antibot"}, {"id": "backup", "name" : "backup"}]}'
|
||||
></div>
|
||||
<div
|
||||
class="hidden"
|
||||
data-server-flash='[{"type" : "success", "title" : "success", "message" : "Success feedback"}, {"type" : "error", "title" : "error", "message" : "Error feedback"}, {"type" : "warning", "title" : "warning", "message" : "Warning feedback"}, {"type" : "info", "title" : "info", "message" : "Info feedback"}]'
|
||||
></div>
|
||||
<div
|
||||
class="hidden"
|
||||
data-server-builder=""
|
||||
></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="home.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You need to change title and the script part like this :
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="/css/flag-icons.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BunkerWeb | Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
class="hidden"
|
||||
data-server-global='{"username" : "admin", "plugins_page": [{"id" : "antibot", "name": "Antibot"}, {"id": "backup", "name" : "backup"}]}'
|
||||
></div>
|
||||
<div
|
||||
class="hidden"
|
||||
data-server-flash='[{"type" : "success", "title" : "success", "message" : "Success feedback"}, {"type" : "error", "title" : "error", "message" : "Error feedback"}, {"type" : "warning", "title" : "warning", "message" : "Warning feedback"}, {"type" : "info", "title" : "info", "message" : "Info feedback"}]'
|
||||
></div>
|
||||
<div
|
||||
class="hidden"
|
||||
data-server-builder=""
|
||||
></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="example.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**2.1 Create custom builder (optional)**
|
||||
|
||||
In case you don't want to use the `BuilderCollection` component but a custom and optimized one for your page, you need to create a new component in the `src/ui/client/dashboard/components/Builder` folder.
|
||||
|
||||
You can copy an existing component like `Collection.vue` and rename it to `Example.vue`, and inside it, you can remove useless components (and add new ones if needed).
|
||||
|
||||
**3. Access page on dev**
|
||||
|
||||
Now you can start dev your page, go to `src/ui/client` and run the following command :
|
||||
|
||||
```shell
|
||||
npm install &&
|
||||
npm run dev-dashboard
|
||||
```
|
||||
|
||||
This will prepare vite and run a dev server, you can access your page on `http://localhost:3000/dashboard/pages/example/index.html`
|
||||
|
||||
**4. Create page builder**
|
||||
|
||||
The UI is using the JSdoc from components to generate a `widgets.py` that allow to create a component using python in a builder approach.
|
||||
In order to create a builder for the new page, you need to go to `src/ui/client/builder/pages` and you can start by copying an existing page folder like `home.py` and rename it to `example.py`.
|
||||
|
||||
**4.1 Create test_builder (optional)**
|
||||
|
||||
In case you want to test your page and the result of the builder, you can create a test file in the `src/ui/client/builder` folder.
|
||||
For example, you can copy the `test_bans.py` file and rename it to `test_example.py`.
|
||||
|
||||
You can import your builder, and run the builder with raw data to see the result.
|
||||
You can also directly update the data on your dev page using the `save_builder` utils.
|
||||
|
||||
**5. Add page to build**
|
||||
|
||||
By default, the new pages will not be added to built app. We need to update the `vite.config.dashboard.js` file and inclue the new page on build :
|
||||
|
||||
```js
|
||||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
|
||||
import { comment } from "postcss";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
// ...
|
||||
build: {
|
||||
// ...
|
||||
rollupOptions: {
|
||||
input: {
|
||||
// ...
|
||||
// Add new page
|
||||
example: resolve(__dirname, "./dashboard/pages/example/index.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**6. Update flask part**
|
||||
|
||||
You need to know that pages and utils from `src/ui/client/builder/` will be accessible on built from the `main.py` file.
|
||||
In order to add the page, you can use the existing `src/ui/pages` folder and follow how they are import in the `main.py` file.
|
||||
|
||||
After that, you will get the new page on the built app.
|
||||
|
||||
### Create a standalone page
|
||||
|
||||
Standalone page is an all-in-one html file with js, css and any resources directly in the file. This is useful for pages that don't need to be part of the dashboard, or for pages that can't access to external resources like the setup page.
|
||||
|
||||
A standalone is similar to a dashboard page, but the differences is that we need a specifig vite config file like `vite.config.standalone.js` that is using `viteSingleFile` plugin to enable a single file build.
|
||||
|
||||
If you want to create your own standalone page, you can copy the standalone folder in `src/ui/client/standalone` and rename it to your page name.
|
||||
You need to create a vite config file based on the `vite.config.standalone.js` updating the build settings.
|
||||
|
||||
In case you want to add the standalone page in built app, you need to update the `build.py` file, and add the output file on the `template` folder **AFTER** the other logic.
|
||||
|
||||
Notice that standalone page is useful for custom plugins pages.
|
||||
|
||||
### Utils
|
||||
|
||||
Because the UI is using a builder approach, we need to create utils that will allow to interact with components or to allow actions without components interaction.
|
||||
|
||||
The utils are located in the `src/ui/client/dashboard/utils` folder, you have the following utils :
|
||||
|
||||
- **`global.js` :** You can find attributes based utils, this is utils that will listen to component attributes. It works well with some components and the `attrs` props. You can attach a link to a button to be redirect, or create a static value form to submit on click...
|
||||
- **`filter.js` :** Filter component utils only.
|
||||
- **`lang.js` :** All logic that is related to i18n, like getting the i18n instance, getting only needed keys, or getting the current language.
|
||||
- **`etc`**
|
||||
|
||||
You have specific logic in `src/ui/client/dashboard/store` too :
|
||||
|
||||
- **`form.js` :** Allow to share and execute specific logic for advanced, raw or easy mode.
|
||||
- **`global.js` :** Others share data and state. For example the `displayStore` is useful because it allows to show/hide an element after a button click.
|
||||
|
|
|
|||
|
|
@ -82,9 +82,9 @@ try:
|
|||
for chunk in resp.iter_content(chunk_size=4 * 1024):
|
||||
if chunk:
|
||||
file_content.write(chunk)
|
||||
|
||||
|
||||
assert file_content
|
||||
|
||||
|
||||
# Decompress it
|
||||
LOGGER.info("Decompressing mmdb file ...")
|
||||
file_content.seek(0)
|
||||
|
|
|
|||
|
|
@ -82,9 +82,9 @@ try:
|
|||
for chunk in resp.iter_content(chunk_size=4 * 1024):
|
||||
if chunk:
|
||||
file_content.write(chunk)
|
||||
|
||||
|
||||
assert file_content
|
||||
|
||||
|
||||
# Decompress it
|
||||
LOGGER.info("Decompressing mmdb file ...")
|
||||
file_content.seek(0)
|
||||
|
|
|
|||
|
|
@ -147,10 +147,12 @@ try:
|
|||
LOGGER.warning(f"Domains for {first_server} are not the same as in the certificate, asking new certificate...")
|
||||
domains_to_ask[first_server] = True
|
||||
continue
|
||||
elif ("TEST_CERT" in current_domains.groupdict()['expiry_date'] and getenv(f"{first_server}_")):
|
||||
elif "TEST_CERT" in current_domains.groupdict()["expiry_date"] and getenv(f"{first_server}_"):
|
||||
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
|
||||
use_letsencrypt_staging = getenv(f"{first_server}_USE_LETS_ENCRYPT_STAGING", getenv("USE_LETS_ENCRYPT_STAGING", "no")) == "yes"
|
||||
if ("TEST_CERT" in current_domains.groupdict()['expiry_date'] and not use_letsencrypt_staging) or ("TEST_CERT" not in current_domains.groupdict()['expiry_date'] and use_letsencrypt_staging):
|
||||
if ("TEST_CERT" in current_domains.groupdict()["expiry_date"] and not use_letsencrypt_staging) or (
|
||||
"TEST_CERT" not in current_domains.groupdict()["expiry_date"] and use_letsencrypt_staging
|
||||
):
|
||||
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
|
||||
domains_to_ask[first_server] = True
|
||||
LOGGER.info(f"Certificates already exists for domain(s) {domains}")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, Logger, _nameToLevel, addLevelName, basicConfig, getLogger, setLoggerClass
|
||||
from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, FileHandler, Formatter, Logger, _nameToLevel, addLevelName, basicConfig, getLogger, setLoggerClass
|
||||
from os import getenv
|
||||
from typing import Optional, Union
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from datetime import datetime
|
|||
from io import BytesIO
|
||||
from itertools import chain
|
||||
from json import load as json_load
|
||||
from logging import FileHandler, Formatter
|
||||
from os import _exit, environ, getenv, getpid, sep
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Dict
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.routes.utils import get_remain, handle_error, manage_bunkerweb, verify_data_in_form, wait_applying
|
||||
from app.utils import LOGGER, flash
|
||||
from app.utils import flash
|
||||
|
||||
|
||||
pro = Blueprint("pro", __name__)
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ button.list-group-item-secondary.active {
|
|||
background-color: var(--bs-bw-green); /* Initial background color */
|
||||
color: #fff;
|
||||
animation: colorPhase 3s infinite; /* Apply the color phasing animation */
|
||||
transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; /* Smooth transitions */
|
||||
transition:
|
||||
background-color 0.3s ease-in-out,
|
||||
box-shadow 0.3s ease-in-out; /* Smooth transitions */
|
||||
}
|
||||
|
||||
.buy-now .btn-buy-now:hover {
|
||||
|
|
@ -520,7 +522,9 @@ a.badge:hover {
|
|||
|
||||
.setting-highlight {
|
||||
background-color: rgba(var(--bs-bw-green-rgb), 0.5);
|
||||
transition: background-color 2s ease, opacity 2s ease;
|
||||
transition:
|
||||
background-color 2s ease,
|
||||
opacity 2s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,14 +95,14 @@ $(document).ready(function () {
|
|||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Time left</div>
|
||||
</li>
|
||||
</ul>`
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-ips-unban").append(list);
|
||||
|
||||
bans.forEach((ban) => {
|
||||
// Create the list item using template literals
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
const listItem =
|
||||
|
|
@ -130,8 +130,8 @@ $(document).ready(function () {
|
|||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to unban the selected IP address${"es".repeat(
|
||||
bans.length > 1
|
||||
)}?`
|
||||
bans.length > 1,
|
||||
)}?`,
|
||||
);
|
||||
modal.show();
|
||||
|
||||
|
|
@ -453,7 +453,7 @@ $(document).ready(function () {
|
|||
$("#bans_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot add bans."
|
||||
"The database is in readonly, therefore you cannot add bans.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
|
|
@ -576,7 +576,7 @@ $(document).ready(function () {
|
|||
});
|
||||
|
||||
const ipRegex = new RegExp(
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?!$)|$)){4}$|^((?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|:(?::[A-Fa-f0-9]{1,4}){1,7}|::)$/i
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?!$)|$)){4}$|^((?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|:(?::[A-Fa-f0-9]{1,4}){1,7}|::)$/i,
|
||||
);
|
||||
|
||||
const validateBan = (ban, ipSet) => {
|
||||
|
|
@ -679,14 +679,14 @@ $(document).ready(function () {
|
|||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "bans",
|
||||
value: JSON.stringify(bans),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
|
|
|
|||
|
|
@ -207,15 +207,15 @@ $(document).ready(function () {
|
|||
});
|
||||
|
||||
$(`#DataTables_Table_0 span[title='${cacheJobNameSelection}']`).trigger(
|
||||
"click"
|
||||
"click",
|
||||
);
|
||||
|
||||
$(`#DataTables_Table_1 span[title='${cachePluginSelection}']`).trigger(
|
||||
"click"
|
||||
"click",
|
||||
);
|
||||
|
||||
$(`#DataTables_Table_2 span[title='${cacheServiceSelection}']`).trigger(
|
||||
"click"
|
||||
"click",
|
||||
);
|
||||
|
||||
$("#cache").removeClass("d-none");
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ $(document).ready(function () {
|
|||
$typeDropdownItems.each(function () {
|
||||
const item = $(this);
|
||||
item.toggle(
|
||||
selectedService === "no service" || item.data("context") === "multisite"
|
||||
selectedService === "no service" ||
|
||||
item.data("context") === "multisite",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
@ -86,13 +87,13 @@ $(document).ready(function () {
|
|||
if (visibleItems === 0) {
|
||||
if ($serviceDropdownMenu.find(".no-service-items").length === 0) {
|
||||
$serviceDropdownMenu.append(
|
||||
'<li class="no-service-items dropdown-item text-muted">No Item</li>'
|
||||
'<li class="no-service-items dropdown-item text-muted">No Item</li>',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$serviceDropdownMenu.find(".no-service-items").remove();
|
||||
}
|
||||
}, 50)
|
||||
}, 50),
|
||||
);
|
||||
|
||||
$(document).on("hidden.bs.dropdown", "#select-service", function () {
|
||||
|
|
@ -107,7 +108,7 @@ $(document).ready(function () {
|
|||
$(`#config-type-${selectedType}`).data("context") !== "multisite"
|
||||
) {
|
||||
const firstMultisiteType = $(
|
||||
`#types-dropdown-menu li.nav-item[data-context="multisite"]`
|
||||
`#types-dropdown-menu li.nav-item[data-context="multisite"]`,
|
||||
).first();
|
||||
$("#select-type")
|
||||
.parent()
|
||||
|
|
@ -115,7 +116,7 @@ $(document).ready(function () {
|
|||
"data-bs-original-title",
|
||||
`Switched to ${firstMultisiteType
|
||||
.text()
|
||||
.trim()} as ${selectedType} is not a valid multisite type.`
|
||||
.trim()} as ${selectedType} is not a valid multisite type.`,
|
||||
)
|
||||
.tooltip("show");
|
||||
|
||||
|
|
@ -167,13 +168,14 @@ $(document).ready(function () {
|
|||
if (!configName) {
|
||||
errorMessage = "A custom configuration name is required.";
|
||||
isValid = false;
|
||||
} else if (pattern && !new RegExp(pattern).test(configName)) isValid = false;
|
||||
} else if (pattern && !new RegExp(pattern).test(configName))
|
||||
isValid = false;
|
||||
|
||||
if (!isValid) {
|
||||
$configInput
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
errorMessage || "Please enter a valid configuration name."
|
||||
errorMessage || "Please enter a valid configuration name.",
|
||||
)
|
||||
.tooltip("show");
|
||||
|
||||
|
|
@ -195,35 +197,35 @@ $(document).ready(function () {
|
|||
type: "hidden",
|
||||
name: "service",
|
||||
value: $("<div>").text(selectedService).html(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "type",
|
||||
value: $("<div>").text(selectedType).html(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "name",
|
||||
value: $("<div>").text(configName).html(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "value",
|
||||
value: $("<div>").text(value).html(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("<div>").text($("#csrf_token").val()).html(), // Sanitize the value
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
$(window).off("beforeunload");
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ $(document).ready(function () {
|
|||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="fw-bold">Service</div>
|
||||
</li>
|
||||
</ul>`
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-configs-delete").append(list);
|
||||
|
||||
configs.forEach((config) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
// Create the list item using template literals
|
||||
|
|
@ -79,13 +79,13 @@ $(document).ready(function () {
|
|||
|
||||
const id = `${config.type.toLowerCase()}-${config.service.replaceAll(
|
||||
".",
|
||||
"_"
|
||||
"_",
|
||||
)}-${config.name}`;
|
||||
|
||||
// Clone the type element and append it to the list item
|
||||
const typeClone = $(`#type-${id}`).clone();
|
||||
const typeListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
|
||||
);
|
||||
typeListItem.append(typeClone.removeClass("highlight"));
|
||||
list.append(typeListItem);
|
||||
|
|
@ -93,7 +93,7 @@ $(document).ready(function () {
|
|||
// Clone the service element and append it to the list item
|
||||
const serviceClone = $(`#service-${id}`).clone();
|
||||
const serviceListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
|
||||
);
|
||||
serviceListItem.append(serviceClone.removeClass("highlight"));
|
||||
list.append(serviceListItem);
|
||||
|
|
@ -107,8 +107,8 @@ $(document).ready(function () {
|
|||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to delete the selected custom configuration${"s".repeat(
|
||||
configs.length > 1
|
||||
)}?`
|
||||
configs.length > 1,
|
||||
)}?`,
|
||||
);
|
||||
modal.show();
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ $(document).ready(function () {
|
|||
$("#configs_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create new custom configurations."
|
||||
"The database is in readonly, therefore you cannot create new custom configurations.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
|
|
@ -439,11 +439,11 @@ $(document).ready(function () {
|
|||
});
|
||||
|
||||
$(`#DataTables_Table_0 span[title='${configTypeSelection}']`).trigger(
|
||||
"click"
|
||||
"click",
|
||||
);
|
||||
|
||||
$(`#DataTables_Table_2 span[title='${configServiceSelection}']`).trigger(
|
||||
"click"
|
||||
"click",
|
||||
);
|
||||
|
||||
$("#configs").removeClass("d-none");
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ $(function () {
|
|||
getColor(from + 1) +
|
||||
'"></i> ' +
|
||||
from +
|
||||
(to ? "–" + to : "+")
|
||||
(to ? "–" + to : "+"),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ $(function () {
|
|||
// Ensure each value is properly converted to a number
|
||||
const totalRequests = Object.values(requestsData).reduce(
|
||||
(acc, curr) => acc + parseInt(curr, 10), // Parse as integer
|
||||
0 // Initial value for the accumulator
|
||||
0, // Initial value for the accumulator
|
||||
);
|
||||
|
||||
const blockedRequests = Object.keys(requestsData).reduce((acc, key) => {
|
||||
|
|
@ -305,7 +305,7 @@ $(function () {
|
|||
|
||||
const requestsChart = new ApexCharts(
|
||||
document.querySelector("#requests-stats"),
|
||||
requestsOptions
|
||||
requestsOptions,
|
||||
);
|
||||
requestsChart.render();
|
||||
|
||||
|
|
@ -396,7 +396,7 @@ $(function () {
|
|||
|
||||
const ipsChart = new ApexCharts(
|
||||
document.querySelector("#requests-ips"),
|
||||
ipsOptions
|
||||
ipsOptions,
|
||||
);
|
||||
ipsChart.render();
|
||||
}
|
||||
|
|
@ -406,10 +406,10 @@ $(function () {
|
|||
const blockingData = JSON.parse($("#requests-blocking-data").text());
|
||||
|
||||
const dataValues = Object.values(blockingData).map((value) =>
|
||||
parseInt(value, 10)
|
||||
parseInt(value, 10),
|
||||
);
|
||||
const categories = Object.keys(blockingData).map((key) =>
|
||||
new Date(key).toLocaleTimeString()
|
||||
new Date(key).toLocaleTimeString(),
|
||||
);
|
||||
|
||||
const minValue = Math.min(...dataValues);
|
||||
|
|
@ -498,7 +498,7 @@ $(function () {
|
|||
|
||||
const blockingChart = new ApexCharts(
|
||||
document.querySelector("#requests-blocking"),
|
||||
blockingOptions
|
||||
blockingOptions,
|
||||
);
|
||||
blockingChart.render();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,14 +75,14 @@ $(document).ready(function () {
|
|||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
|
||||
<div class="fw-bold">Health</div>
|
||||
</li>
|
||||
</ul>`
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-instances").append(list);
|
||||
|
||||
const delete_modal = $("#modal-delete-instances");
|
||||
instances.forEach((instance) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
// Create the list item using template literals
|
||||
|
|
@ -97,7 +97,7 @@ $(document).ready(function () {
|
|||
// Clone the status element and append it to the list item
|
||||
const statusClone = $("#status-" + instance).clone();
|
||||
const statusListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`
|
||||
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`,
|
||||
);
|
||||
statusListItem.append(statusClone.removeClass("highlight"));
|
||||
list.append(statusListItem);
|
||||
|
|
@ -124,14 +124,14 @@ $(document).ready(function () {
|
|||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "instances",
|
||||
value: instances.join(","),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
|
|
@ -564,7 +564,7 @@ $(document).ready(function () {
|
|||
$("#instances_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create new instances."
|
||||
"The database is in readonly, therefore you cannot create new instances.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
|
|
|
|||
|
|
@ -121,14 +121,14 @@ $(document).ready(function () {
|
|||
type: "hidden",
|
||||
name: "csrf_token",
|
||||
value: $("#csrf_token").val(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
form.append(
|
||||
$("<input>", {
|
||||
type: "hidden",
|
||||
name: "jobs",
|
||||
value: JSON.stringify(jobs),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Append the form to the body and submit it
|
||||
|
|
@ -369,7 +369,7 @@ $(document).ready(function () {
|
|||
.html(
|
||||
`Last${historyCount > 1 ? " " + historyCount : ""} execution${
|
||||
historyCount > 1 ? "s" : ""
|
||||
} of Job <span class="fw-bold fst-italic">${job}</span> from plugin <span class="fw-bold fst-italic">${plugin}</span>`
|
||||
} of Job <span class="fw-bold fst-italic">${job}</span> from plugin <span class="fw-bold fst-italic">${plugin}</span>`,
|
||||
);
|
||||
history.removeClass("visually-hidden");
|
||||
historyModal.find(".modal-body").html(history);
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ $(document).ready(function () {
|
|||
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 1 0;">
|
||||
<div class="fw-bold">Type</div>
|
||||
</li>
|
||||
</ul>`
|
||||
</ul>`,
|
||||
);
|
||||
$("#selected-plugins-delete").append(list);
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
const list = $(
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`
|
||||
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
|
||||
);
|
||||
|
||||
// Create the list item using template literals
|
||||
|
|
@ -42,7 +42,7 @@ $(document).ready(function () {
|
|||
// Clone the version element and append it to the list item
|
||||
const versionClone = $(`#version-${plugin}`).clone();
|
||||
const versionListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
|
||||
);
|
||||
versionListItem.append(versionClone.removeClass("highlight"));
|
||||
list.append(versionListItem);
|
||||
|
|
@ -50,7 +50,7 @@ $(document).ready(function () {
|
|||
// Clone the type element and append it to the list item
|
||||
const typeClone = $(`#type-${plugin}`).clone();
|
||||
const typeListItem = $(
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`
|
||||
`<li class="list-group-item d-flex align-items-center" style="flex: 1 1 0;"></li>`,
|
||||
);
|
||||
typeListItem.append(typeClone.removeClass("highlight"));
|
||||
list.append(typeListItem);
|
||||
|
|
@ -64,8 +64,8 @@ $(document).ready(function () {
|
|||
.find(".alert")
|
||||
.text(
|
||||
`Are you sure you want to delete the selected plugin${"s".repeat(
|
||||
plugins.length > 1
|
||||
)}?`
|
||||
plugins.length > 1,
|
||||
)}?`,
|
||||
);
|
||||
modal.show();
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ $(document).ready(function () {
|
|||
|
||||
// Create a progress bar element
|
||||
const progressBar = $(
|
||||
'<div class="progress-bar" role="progressbar" style="width: 0%;"></div>'
|
||||
'<div class="progress-bar" role="progressbar" style="width: 0%;"></div>',
|
||||
);
|
||||
const progress = $('<div class="progress mt-2"></div>').append(progressBar);
|
||||
fileList.append(progress);
|
||||
|
|
@ -447,7 +447,7 @@ $(document).ready(function () {
|
|||
$("#plugins_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create add plugins."
|
||||
"The database is in readonly, therefore you cannot create add plugins.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ $(function () {
|
|||
if (country) {
|
||||
$(`[data-bs-original-title="${countryCode}"]`).attr(
|
||||
"data-bs-original-title",
|
||||
country
|
||||
country,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ $(function () {
|
|||
services.forEach((service) => {
|
||||
const sanitizedService = service.replace(/\./g, "-");
|
||||
const serviceList = $(
|
||||
'<ul class="list-group list-group-horizontal d-flex w-100"></ul>'
|
||||
'<ul class="list-group list-group-horizontal d-flex w-100"></ul>',
|
||||
);
|
||||
|
||||
const listItem = $(`
|
||||
|
|
@ -57,7 +57,7 @@ $(function () {
|
|||
.text(
|
||||
`Are you sure you want to convert the selected service${
|
||||
services.length > 1 ? "s" : ""
|
||||
} to ${conversionType}?`
|
||||
} to ${conversionType}?`,
|
||||
);
|
||||
convertModal
|
||||
.find("button[type=submit]")
|
||||
|
|
@ -78,7 +78,7 @@ $(function () {
|
|||
.text(
|
||||
`Are you sure you want to delete the selected service${
|
||||
services.length > 1 ? "s" : ""
|
||||
}?`
|
||||
}?`,
|
||||
);
|
||||
const modalInstance = new bootstrap.Modal(deleteModal);
|
||||
modalInstance.show();
|
||||
|
|
@ -184,10 +184,10 @@ $(function () {
|
|||
.empty();
|
||||
$(this)
|
||||
.find(
|
||||
"#selected-services-input-convert, #selected-services-input-delete"
|
||||
"#selected-services-input-convert, #selected-services-input-delete",
|
||||
)
|
||||
.val("");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const getSelectedServices = () =>
|
||||
|
|
@ -230,7 +230,7 @@ $(function () {
|
|||
|
||||
const filteredServices = services.filter((service) => {
|
||||
const serviceType = $(`#type-${service.replace(/\./g, "-")}`).data(
|
||||
"value"
|
||||
"value",
|
||||
);
|
||||
return serviceType !== conversionType;
|
||||
});
|
||||
|
|
@ -408,7 +408,7 @@ $(function () {
|
|||
.find(".dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in read-only mode; you cannot create new services."
|
||||
"The database is in read-only mode; you cannot create new services.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
|
|
|
|||
|
|
@ -33,22 +33,22 @@ $(document).ready(() => {
|
|||
isValid = validateCondition(
|
||||
password.length >= 8,
|
||||
"#length-check i",
|
||||
isValid
|
||||
isValid,
|
||||
);
|
||||
isValid = validateCondition(
|
||||
/[A-Z]/.test(password),
|
||||
"#uppercase-check i",
|
||||
isValid
|
||||
isValid,
|
||||
);
|
||||
isValid = validateCondition(
|
||||
/\d/.test(password),
|
||||
"#number-check i",
|
||||
isValid
|
||||
isValid,
|
||||
);
|
||||
isValid = validateCondition(
|
||||
/[ -~]/.test(password),
|
||||
"#special-check i",
|
||||
isValid
|
||||
isValid,
|
||||
); // Check for special characters
|
||||
|
||||
return isValid;
|
||||
|
|
@ -165,7 +165,7 @@ $(document).ready(() => {
|
|||
if (!$feedback.length) {
|
||||
const $textSpan = $input.parent().find("span.input-group-text");
|
||||
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
|
||||
$textSpan.length ? $textSpan : $input
|
||||
$textSpan.length ? $textSpan : $input,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ $(document).ready(() => {
|
|||
.find("i")
|
||||
.toggleClass(
|
||||
"bx-question-mark text-warning bx-check text-success",
|
||||
false
|
||||
false,
|
||||
)
|
||||
.toggleClass("bx-x text-danger", true);
|
||||
} else {
|
||||
|
|
@ -255,7 +255,7 @@ $(document).ready(() => {
|
|||
if (!$feedback.length) {
|
||||
const $textSpan = $input.parent().find("span.input-group-text");
|
||||
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
|
||||
$textSpan.length ? $textSpan : $input
|
||||
$textSpan.length ? $textSpan : $input,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ $(document).ready(() => {
|
|||
}
|
||||
if (!uiReverseProxy) {
|
||||
$("#overview_service_url").val(
|
||||
`https://${getServerName()}${$("#REVERSE_PROXY_URL").val()}`
|
||||
`https://${getServerName()}${$("#REVERSE_PROXY_URL").val()}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -403,7 +403,7 @@ $(document).ready(() => {
|
|||
.parent()
|
||||
.find("span.input-group-text");
|
||||
$feedback = $(
|
||||
'<div class="invalid-feedback">Passwords do not match.</div>'
|
||||
'<div class="invalid-feedback">Passwords do not match.</div>',
|
||||
).insertAfter($textSpan.length ? $textSpan : $confirmPasswordInput);
|
||||
} else {
|
||||
$feedback.text("Passwords do not match.");
|
||||
|
|
@ -424,13 +424,13 @@ $(document).ready(() => {
|
|||
if (typeof result === "string") {
|
||||
$("#dns-check-title").text("Error");
|
||||
$("#dns-check-result").html(
|
||||
`Are you sure you want to proceed to the next step?<br/>Error: ${result}`
|
||||
`Are you sure you want to proceed to the next step?<br/>Error: ${result}`,
|
||||
);
|
||||
modal.modal("show");
|
||||
} else if (!result) {
|
||||
$("#dns-check-title").text("Server name is not unique");
|
||||
$("#dns-check-result").html(
|
||||
`Are you sure you want to proceed to the next step?<br/>Server name "${serverName}" is not unique.`
|
||||
`Are you sure you want to proceed to the next step?<br/>Server name "${serverName}" is not unique.`,
|
||||
);
|
||||
modal.modal("show");
|
||||
} else {
|
||||
|
|
@ -456,7 +456,7 @@ $(document).ready(() => {
|
|||
debounce(function () {
|
||||
const isValid = validatePassword();
|
||||
updateValidationState(this, isValid);
|
||||
}, 100)
|
||||
}, 100),
|
||||
);
|
||||
|
||||
// Real-time validation for other plugin settings
|
||||
|
|
@ -471,7 +471,7 @@ $(document).ready(() => {
|
|||
$this
|
||||
.toggleClass("is-valid", isValid)
|
||||
.toggleClass("is-invalid", !isValid);
|
||||
}, 100)
|
||||
}, 100),
|
||||
);
|
||||
|
||||
// Remove validation state on focus out
|
||||
|
|
@ -509,11 +509,11 @@ $(document).ready(() => {
|
|||
formData.append("ui_url", ui_url);
|
||||
formData.append(
|
||||
"auto_lets_encrypt",
|
||||
$("#AUTO_LETS_ENCRYPT").prop("checked") ? "yes" : "no"
|
||||
$("#AUTO_LETS_ENCRYPT").prop("checked") ? "yes" : "no",
|
||||
);
|
||||
formData.append(
|
||||
"lets_encrypt_staging",
|
||||
$("#LETS_ENCRYPT_STAGING").prop("checked") ? "yes" : "no"
|
||||
$("#LETS_ENCRYPT_STAGING").prop("checked") ? "yes" : "no",
|
||||
);
|
||||
formData.append("email_lets_encrypt", $("#EMAIL_LETS_ENCRYPT").val());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ $(document).ready(() => {
|
|||
if ($collapseContainer.length && !$collapseContainer.hasClass("show")) {
|
||||
// Expand the multiple setting group if it's collapsed
|
||||
const toggleButton = $(
|
||||
`[data-bs-target="#${$collapseContainer.attr("id")}"]`
|
||||
`[data-bs-target="#${$collapseContainer.attr("id")}"]`,
|
||||
);
|
||||
toggleButton.trigger("click");
|
||||
}
|
||||
|
|
@ -195,7 +195,7 @@ $(document).ready(() => {
|
|||
let $feedback = $input.next(".invalid-feedback");
|
||||
if (!$feedback.length) {
|
||||
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
|
||||
$input
|
||||
$input,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ $(document).ready(() => {
|
|||
type: "hidden",
|
||||
name: name,
|
||||
value: $("<div>").text(value).html(), // Sanitize the value
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -400,13 +400,13 @@ $(document).ready(() => {
|
|||
if (visibleItems === 0) {
|
||||
if ($pluginDropdownMenu.find(".no-plugin-items").length === 0) {
|
||||
$pluginDropdownMenu.append(
|
||||
'<li class="no-plugin-items dropdown-item text-muted">No Item</li>'
|
||||
'<li class="no-plugin-items dropdown-item text-muted">No Item</li>',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$pluginDropdownMenu.find(".no-plugin-items").remove();
|
||||
}
|
||||
}, 50)
|
||||
}, 50),
|
||||
);
|
||||
|
||||
$("#select-template").on("click", () => $templateSearch.focus());
|
||||
|
|
@ -431,13 +431,13 @@ $(document).ready(() => {
|
|||
if (visibleItems === 0) {
|
||||
if ($templateDropdownMenu.find(".no-template-items").length === 0) {
|
||||
$templateDropdownMenu.append(
|
||||
'<li class="no-template-items dropdown-item text-muted">No Item</li>'
|
||||
'<li class="no-template-items dropdown-item text-muted">No Item</li>',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$templateDropdownMenu.find(".no-template-items").remove();
|
||||
}
|
||||
}, 50)
|
||||
}, 50),
|
||||
);
|
||||
|
||||
$(document).on("hidden.bs.dropdown", "#select-plugin", function () {
|
||||
|
|
@ -453,21 +453,21 @@ $(document).ready(() => {
|
|||
"shown.bs.tab",
|
||||
(e) => {
|
||||
handleModeChange($(e.target).data("bs-target"));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$('#plugins-dropdown-menu button[data-bs-toggle="tab"]').on(
|
||||
"shown.bs.tab",
|
||||
(e) => {
|
||||
handleTabChange($(e.target).data("bs-target"));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$('#templates-dropdown-menu button[data-bs-toggle="tab"]').on(
|
||||
"shown.bs.tab",
|
||||
(e) => {
|
||||
handleTabChange($(e.target).data("bs-target"));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$(document).on("input", ".plugin-setting", function () {
|
||||
|
|
@ -553,7 +553,7 @@ $(document).ready(() => {
|
|||
if (matchedPlugin) {
|
||||
// Automatically switch to the plugin tab
|
||||
$(`button[data-bs-target="#navs-plugins-${matchedPlugin}"]`).tab(
|
||||
"show"
|
||||
"show",
|
||||
);
|
||||
|
||||
// Highlight all matched settings
|
||||
|
|
@ -561,7 +561,7 @@ $(document).ready(() => {
|
|||
highlightSettings(matchedSettings, 1000);
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}, 100),
|
||||
);
|
||||
|
||||
$(document).on("click", ".show-multiple", function () {
|
||||
|
|
@ -569,7 +569,7 @@ $(document).ready(() => {
|
|||
$(this).html(
|
||||
`<i class="bx bx-${
|
||||
toggleText === "SHOW" ? "hide" : "show-alt"
|
||||
} bx-sm"></i> ${toggleText}`
|
||||
} bx-sm"></i> ${toggleText}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -584,7 +584,7 @@ $(document).ready(() => {
|
|||
$(this)
|
||||
.find(".multiple-collapse")
|
||||
.attr("id")
|
||||
.replace(`${multipleId}-`, "")
|
||||
.replace(`${multipleId}-`, ""),
|
||||
);
|
||||
})
|
||||
.get()
|
||||
|
|
@ -696,7 +696,7 @@ $(document).ready(() => {
|
|||
$(this)
|
||||
.find(".multiple-collapse")
|
||||
.attr("id")
|
||||
.replace(`${multipleId}-`, "")
|
||||
.replace(`${multipleId}-`, ""),
|
||||
);
|
||||
if (containerSuffix > suffix) {
|
||||
$(this).before(multipleClone); // Insert before the first container with a higher suffix
|
||||
|
|
@ -797,7 +797,7 @@ $(document).ready(() => {
|
|||
$(".toggle-draft").html(
|
||||
`<i class="bx bx-sm bx-${
|
||||
isDraft ? "globe" : "file-blank"
|
||||
} bx-sm"></i> ${isDraft ? "Online" : "Draft"}`
|
||||
} bx-sm"></i> ${isDraft ? "Online" : "Draft"}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -961,7 +961,7 @@ $(document).ready(() => {
|
|||
.parent()
|
||||
.attr(
|
||||
"title",
|
||||
"Cannot remove because one or more settings are disabled"
|
||||
"Cannot remove because one or more settings are disabled",
|
||||
);
|
||||
|
||||
new bootstrap.Tooltip(
|
||||
|
|
@ -970,7 +970,7 @@ $(document).ready(() => {
|
|||
.get(0),
|
||||
{
|
||||
placement: "top",
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -986,7 +986,7 @@ $(document).ready(() => {
|
|||
|
||||
if (currentMode === "easy" && currentTemplate !== "high") {
|
||||
$(`button[data-bs-target="#navs-templates-${currentTemplate}"]`).tab(
|
||||
"show"
|
||||
"show",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1012,7 +1012,7 @@ $(document).ready(() => {
|
|||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const targetTab = $(
|
||||
`button[data-bs-target="#navs-plugins-${hash.substring(1)}"]`
|
||||
`button[data-bs-target="#navs-plugins-${hash.substring(1)}"]`,
|
||||
);
|
||||
if (targetTab.length) targetTab.tab("show");
|
||||
}
|
||||
|
|
@ -1029,7 +1029,7 @@ $(document).ready(() => {
|
|||
$pluginTypeSelect.val("all");
|
||||
} else
|
||||
$(`button[data-bs-target="#navs-plugins-${currentPlugin}"]`).tab(
|
||||
"show"
|
||||
"show",
|
||||
);
|
||||
|
||||
if (currentPlugin !== "general") {
|
||||
|
|
@ -1053,7 +1053,7 @@ $(document).ready(() => {
|
|||
feedbackToast
|
||||
.find("div.toast-body")
|
||||
.html(
|
||||
"<p>As the service method is set to autoconf, the configuration is locked. <div class='fw-bolder'>Any changes made will not be saved.</div><div class='fst-italic'>This is to prevent conflicts with the autoconf and the web UI.</div></p>"
|
||||
"<p>As the service method is set to autoconf, the configuration is locked. <div class='fw-bolder'>Any changes made will not be saved.</div><div class='fst-italic'>This is to prevent conflicts with the autoconf and the web UI.</div></p>",
|
||||
);
|
||||
feedbackToast.attr("data-bs-autohide", "false");
|
||||
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
|
||||
|
|
|
|||
|
|
@ -35,20 +35,20 @@ class News {
|
|||
) {
|
||||
sessionStorage.setItem(
|
||||
"lastRefetch",
|
||||
Math.round(Date.now() / 1000) + 3600
|
||||
Math.round(Date.now() / 1000) + 3600,
|
||||
);
|
||||
sessionStorage.setItem("lastNews", JSON.stringify(lastNews));
|
||||
|
||||
const newsNumber = lastNews.length;
|
||||
$("#news-pill").append(
|
||||
DOMPurify.sanitize(
|
||||
`<span class="badge rounded-pill badge-center-sm bg-danger ms-1_5">${newsNumber}</span>`
|
||||
)
|
||||
`<span class="badge rounded-pill badge-center-sm bg-danger ms-1_5">${newsNumber}</span>`,
|
||||
),
|
||||
);
|
||||
$("#news-button").after(
|
||||
DOMPurify.sanitize(
|
||||
`<span class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">${newsNumber}<span class="visually-hidden">unread news</span></span>`
|
||||
)
|
||||
`<span class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">${newsNumber}<span class="visually-hidden">unread news</span></span>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ class News {
|
|||
news.tags,
|
||||
news.date,
|
||||
isLast,
|
||||
false // isHome is false
|
||||
false, // isHome is false
|
||||
);
|
||||
newsRow.append(cardElement);
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ class News {
|
|||
news.tags,
|
||||
news.date,
|
||||
isLast,
|
||||
true // isHome is true
|
||||
true, // isHome is true
|
||||
);
|
||||
homeNewsRow.append(homeCardElement);
|
||||
}
|
||||
|
|
@ -131,13 +131,13 @@ class News {
|
|||
class: "card-img card-img-left",
|
||||
src: img,
|
||||
alt: "News image",
|
||||
})
|
||||
}),
|
||||
);
|
||||
imgCol.append(imgLink);
|
||||
|
||||
const contentCol = $("<div>", { class: "col-md-7" }).appendTo(row);
|
||||
const cardBody = $("<div>", { class: "card-body p-3" }).appendTo(
|
||||
contentCol
|
||||
contentCol,
|
||||
);
|
||||
|
||||
const cardTitle = $("<h6>", { class: "card-title lh-sm mb-2" }).append(
|
||||
|
|
@ -146,7 +146,7 @@ class News {
|
|||
target: "_blank",
|
||||
rel: "noopener",
|
||||
text: title,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const cardText = $("<small>", {
|
||||
|
|
@ -163,7 +163,7 @@ class News {
|
|||
$("<small>", {
|
||||
class: "text-muted courier-prime",
|
||||
text: `Posted on: ${date}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.appendTo(cardFooter);
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ class News {
|
|||
$("<span>", {
|
||||
class: "tf-icons bx bx-xs bx-purchase-tag me-1",
|
||||
}),
|
||||
tag.name
|
||||
tag.name,
|
||||
)
|
||||
.appendTo(tagsContainer);
|
||||
});
|
||||
|
|
@ -201,7 +201,7 @@ class News {
|
|||
class: "card-img-top",
|
||||
src: img,
|
||||
alt: "News image",
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const cardBody = $("<div>", { class: "card-body" });
|
||||
|
|
@ -211,7 +211,7 @@ class News {
|
|||
target: "_blank",
|
||||
rel: "noopener",
|
||||
text: title,
|
||||
})
|
||||
}),
|
||||
);
|
||||
const cardText = $("<p>", {
|
||||
class: "card-text courier-prime",
|
||||
|
|
@ -232,7 +232,7 @@ class News {
|
|||
$("<span>", {
|
||||
class: "tf-icons bx bx-xs bx-purchase-tag bx-18px me-2",
|
||||
}),
|
||||
tag.name
|
||||
tag.name,
|
||||
)
|
||||
.appendTo(tagsContainer);
|
||||
});
|
||||
|
|
@ -241,7 +241,7 @@ class News {
|
|||
$("<small>", {
|
||||
class: "text-muted courier-prime",
|
||||
text: `Posted on: ${date}`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
cardBody.append(cardTitle, cardText, tagsContainer, dateText);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -61,4 +61,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,4 +60,4 @@
|
|||
"reset": "Tetapkan Semula Zum"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,4 +60,4 @@
|
|||
"reset": "Đặt lại thu phóng"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="d-flex align-items-center justify-content-center h-100">
|
||||
<div class="text-center text-primary">
|
||||
<p id="config-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Coming soon...</p>
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Plugin pages are not yet available during the beta phase.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ services:
|
|||
volumes:
|
||||
bw-data:
|
||||
|
||||
|
||||
networks:
|
||||
bw-universe:
|
||||
name: bw-universe
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ services:
|
|||
volumes:
|
||||
bw-data:
|
||||
|
||||
|
||||
networks:
|
||||
bw-universe:
|
||||
name: bw-universe
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ services:
|
|||
volumes:
|
||||
bw-data:
|
||||
|
||||
|
||||
networks:
|
||||
bw-universe:
|
||||
name: bw-universe
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ volumes:
|
|||
bw-data:
|
||||
bw-db:
|
||||
|
||||
|
||||
networks:
|
||||
bw-universe:
|
||||
name: bw-universe
|
||||
|
|
|
|||
Loading…
Reference in a new issue