Start huge UI refactor
|
|
@ -61,8 +61,8 @@ repos:
|
|||
hooks:
|
||||
- id: codespell
|
||||
name: Codespell Spell Checker
|
||||
exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*|src/ui/static/js/lottie-web.min.js)$
|
||||
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip src/ui/static/js/utils/flatpickr.js,src/ui/static/css/style.css,CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py
|
||||
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
|
||||
language: python
|
||||
types: [text]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
mike==2.1.3
|
||||
mkdocs-material[imaging]==9.5.32
|
||||
mkdocs-material[imaging]==9.5.33
|
||||
mkdocs-print-site-plugin==2.5.0
|
||||
pytablewriter==1.2.0
|
||||
|
|
|
|||
|
|
@ -211,21 +211,21 @@ ghp-import==2.1.0 \
|
|||
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
|
||||
--hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
|
||||
# via mkdocs
|
||||
idna==3.7 \
|
||||
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
|
||||
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
|
||||
idna==3.8 \
|
||||
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
|
||||
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
|
||||
# via requests
|
||||
importlib-metadata==8.3.0 \
|
||||
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
|
||||
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
|
||||
importlib-metadata==8.4.0 \
|
||||
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
|
||||
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
|
||||
# via
|
||||
# markdown
|
||||
# mike
|
||||
# mkdocs
|
||||
# mkdocs-get-deps
|
||||
importlib-resources==6.4.3 \
|
||||
--hash=sha256:2d6dfe3b9e055f72495c2085890837fc8c758984e209115c8792bddcb762cd93 \
|
||||
--hash=sha256:4a202b9b9d38563b46da59221d77bb73862ab5d79d461307bcb826d725448b98
|
||||
importlib-resources==6.4.4 \
|
||||
--hash=sha256:20600c8b7361938dc0bb2d5ec0297802e575df486f5a544fa414da65e13721f7 \
|
||||
--hash=sha256:dda242603d1c9cd836c3368b1174ed74cb4049ecd209e7a1a0104620c18c5c11
|
||||
# via mike
|
||||
jinja2==3.1.4 \
|
||||
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
|
||||
|
|
@ -332,9 +332,9 @@ mkdocs-get-deps==0.2.0 \
|
|||
--hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \
|
||||
--hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134
|
||||
# via mkdocs
|
||||
mkdocs-material==9.5.32 \
|
||||
--hash=sha256:38ed66e6d6768dde4edde022554553e48b2db0d26d1320b19e2e2b9da0be1120 \
|
||||
--hash=sha256:f3704f46b63d31b3cd35c0055a72280bed825786eccaf19c655b44e0cd2c6b3f
|
||||
mkdocs-material==9.5.33 \
|
||||
--hash=sha256:d23a8b5e3243c9b2f29cdfe83051104a8024b767312dc8fde05ebe91ad55d89d \
|
||||
--hash=sha256:dbc79cf0fdc6e2c366aa987de8b0c9d4e2bb9f156e7466786ba2fd0f9bf7ffca
|
||||
# via
|
||||
# -r requirements.in
|
||||
# mkdocs-print-site-plugin
|
||||
|
|
@ -352,16 +352,17 @@ packaging==24.1 \
|
|||
# via
|
||||
# mkdocs
|
||||
# typepy
|
||||
paginate==0.5.6 \
|
||||
--hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d
|
||||
paginate==0.5.7 \
|
||||
--hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
|
||||
--hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
|
||||
# via mkdocs-material
|
||||
pathspec==0.12.1 \
|
||||
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
|
||||
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
|
||||
# via mkdocs
|
||||
pathvalidate==3.2.0 \
|
||||
--hash=sha256:5e8378cf6712bff67fbe7a8307d99fa8c1a0cb28aa477056f8fc374f0dff24ad \
|
||||
--hash=sha256:cc593caa6299b22b37f228148257997e2fa850eea2daf7e4cc9205cef6908dee
|
||||
pathvalidate==3.2.1 \
|
||||
--hash=sha256:9a6255eb8f63c9e2135b9be97a5ce08f10230128c4ae7b3e935378b82b22c4c9 \
|
||||
--hash=sha256:f5d07b1e2374187040612a1fcd2bcb2919f8db180df254c9581bb90bf903377d
|
||||
# via pytablewriter
|
||||
pillow==10.4.0 \
|
||||
--hash=sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885 \
|
||||
|
|
@ -463,9 +464,9 @@ pymdown-extensions==10.9 \
|
|||
--hash=sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753 \
|
||||
--hash=sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626
|
||||
# via mkdocs-material
|
||||
pyparsing==3.1.2 \
|
||||
--hash=sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad \
|
||||
--hash=sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742
|
||||
pyparsing==3.1.4 \
|
||||
--hash=sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c \
|
||||
--hash=sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032
|
||||
# via mike
|
||||
pytablewriter==1.2.0 \
|
||||
--hash=sha256:0204a4bb684a22140d640f2599f09e137bcdc18b3dd49426f4a555016e246b46 \
|
||||
|
|
@ -636,9 +637,9 @@ requests==2.32.3 \
|
|||
# importlib-resources
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
setuptools==73.0.0 \
|
||||
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
|
||||
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
|
||||
setuptools==73.0.1 \
|
||||
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
|
||||
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
|
||||
# via mkdocs-material
|
||||
six==1.16.0 \
|
||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
||||
|
|
@ -716,7 +717,7 @@ webencodings==0.5.1 \
|
|||
# via
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
zipp==3.20.0 \
|
||||
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
|
||||
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
|
||||
zipp==3.20.1 \
|
||||
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
|
||||
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
|
||||
# via pytablewriter
|
||||
|
|
|
|||
|
|
@ -103,13 +103,9 @@ services:
|
|||
dockerfile: ./src/ui/Dockerfile
|
||||
volumes:
|
||||
- bw-logs:/var/log/bunkerweb
|
||||
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
|
||||
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
|
||||
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
|
||||
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
|
||||
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
|
||||
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
|
||||
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
|
||||
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
|
||||
environment:
|
||||
<<: *env
|
||||
ADMIN_USERNAME: "admin"
|
||||
|
|
|
|||
|
|
@ -100,13 +100,9 @@ services:
|
|||
dockerfile: ./src/ui/Dockerfile
|
||||
volumes:
|
||||
- bw-logs:/var/log/bunkerweb
|
||||
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
|
||||
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
|
||||
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
|
||||
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
|
||||
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
|
||||
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
|
||||
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
|
||||
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
|
||||
environment:
|
||||
<<: *env
|
||||
ADMIN_USERNAME: "admin"
|
||||
|
|
|
|||
|
|
@ -100,13 +100,9 @@ services:
|
|||
dockerfile: ./src/ui/Dockerfile
|
||||
volumes:
|
||||
- bw-logs:/var/log/bunkerweb
|
||||
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
|
||||
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
|
||||
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
|
||||
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
|
||||
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
|
||||
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
|
||||
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
|
||||
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
|
||||
environment:
|
||||
<<: *env
|
||||
DEBUG: "1"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ services:
|
|||
DISABLE_DEFAULT_SERVER: "yes"
|
||||
USE_CLIENT_CACHE: "yes"
|
||||
USE_GZIP: "yes"
|
||||
USE_MODSECURITY: "no"
|
||||
USE_BAD_BEHAVIOR: "no"
|
||||
USE_LIMIT_REQ: "no"
|
||||
USE_LIMIT_CONN: "no"
|
||||
EXTERNAL_PLUGIN_URLS: "https://github.com/bunkerity/bunkerweb-plugins/archive/refs/heads/dev.zip"
|
||||
CUSTOM_CONF_MODSEC_CRS_reqbody-suppress: "SecRuleRemoveById 200002"
|
||||
www.example.com_USE_UI: "yes"
|
||||
|
|
@ -85,13 +89,9 @@ services:
|
|||
dockerfile: ./src/ui/Dockerfile
|
||||
volumes:
|
||||
- bw-logs:/var/log/bunkerweb
|
||||
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
|
||||
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
|
||||
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
|
||||
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
|
||||
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
|
||||
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
|
||||
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
|
||||
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
|
||||
environment:
|
||||
<<: *env
|
||||
ADMIN_USERNAME: "admin"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ services:
|
|||
DISABLE_DEFAULT_SERVER: "yes"
|
||||
USE_CLIENT_CACHE: "yes"
|
||||
USE_GZIP: "yes"
|
||||
USE_MODSECURITY: "no"
|
||||
USE_BAD_BEHAVIOR: "no"
|
||||
USE_LIMIT_REQ: "no"
|
||||
USE_LIMIT_CONN: "no"
|
||||
www.example.com_USE_UI: "yes"
|
||||
www.example.com_USE_REVERSE_PROXY: "yes"
|
||||
www.example.com_REVERSE_PROXY_URL: "/admin"
|
||||
|
|
@ -82,13 +86,9 @@ services:
|
|||
dockerfile: ./src/ui/Dockerfile
|
||||
volumes:
|
||||
- bw-logs:/var/log/bunkerweb
|
||||
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
|
||||
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
|
||||
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
|
||||
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
|
||||
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
|
||||
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
|
||||
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
|
||||
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
|
||||
environment:
|
||||
<<: *env
|
||||
ADMIN_USERNAME: "admin"
|
||||
|
|
|
|||
|
|
@ -74,13 +74,9 @@ services:
|
|||
dockerfile: ./src/ui/Dockerfile
|
||||
volumes:
|
||||
- bw-logs:/var/log/bunkerweb
|
||||
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
|
||||
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
|
||||
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
|
||||
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
|
||||
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
|
||||
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
|
||||
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
|
||||
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
|
||||
environment:
|
||||
<<: *env
|
||||
DEBUG: "1"
|
||||
|
|
|
|||
|
|
@ -114,9 +114,9 @@ google-auth==2.34.0 \
|
|||
--hash=sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65 \
|
||||
--hash=sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc
|
||||
# via kubernetes
|
||||
idna==3.7 \
|
||||
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
|
||||
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
|
||||
idna==3.8 \
|
||||
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
|
||||
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
|
||||
# via requests
|
||||
kubernetes==30.1.0 \
|
||||
--hash=sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc \
|
||||
|
|
|
|||
|
|
@ -104,9 +104,9 @@ charset-normalizer==3.3.2 \
|
|||
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
|
||||
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
|
||||
# via requests
|
||||
idna==3.7 \
|
||||
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
|
||||
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
|
||||
idna==3.8 \
|
||||
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
|
||||
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
|
||||
# via requests
|
||||
jinja2==3.1.4 \
|
||||
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ pip==24.2
|
|||
pip-compile-multi==2.6.4
|
||||
pip-tools==7.4.1
|
||||
pip-upgrader==1.4.15
|
||||
setuptools==73.0.0
|
||||
setuptools==73.0.1
|
||||
tomli==2.0.1
|
||||
wheel==0.44.0
|
||||
|
|
|
|||
|
|
@ -117,13 +117,13 @@ colorclass==2.2.2 \
|
|||
docopt==0.6.2 \
|
||||
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
|
||||
# via pip-upgrader
|
||||
idna==3.7 \
|
||||
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
|
||||
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
|
||||
idna==3.8 \
|
||||
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
|
||||
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
|
||||
# via requests
|
||||
importlib-metadata==8.3.0 \
|
||||
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
|
||||
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
|
||||
importlib-metadata==8.4.0 \
|
||||
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
|
||||
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
|
||||
# via build
|
||||
packaging==24.1 \
|
||||
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
|
||||
|
|
@ -163,9 +163,9 @@ requests==2.32.3 \
|
|||
# via
|
||||
# -r requirements-deps.in
|
||||
# pip-tools
|
||||
setuptools==73.0.0 \
|
||||
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
|
||||
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
|
||||
setuptools==73.0.1 \
|
||||
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
|
||||
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
|
||||
# via pip-upgrader
|
||||
terminaltables==3.1.10 \
|
||||
--hash=sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543 \
|
||||
|
|
@ -192,9 +192,9 @@ wheel==0.44.0 \
|
|||
# via
|
||||
# -r requirements-deps.in
|
||||
# pip-tools
|
||||
zipp==3.20.0 \
|
||||
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
|
||||
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
|
||||
zipp==3.20.1 \
|
||||
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
|
||||
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
|
||||
# via
|
||||
# -r requirements-deps.in
|
||||
# pip-tools
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
pip==24.2
|
||||
pip-tools==7.4.1
|
||||
setuptools==73.0.0
|
||||
setuptools==73.0.1
|
||||
wheel==0.44.0
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ click==8.1.7 \
|
|||
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
|
||||
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
|
||||
# via pip-tools
|
||||
importlib-metadata==8.3.0 \
|
||||
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
|
||||
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
|
||||
importlib-metadata==8.4.0 \
|
||||
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
|
||||
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
|
||||
# via build
|
||||
packaging==24.1 \
|
||||
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
|
||||
|
|
@ -36,9 +36,9 @@ pyproject-hooks==1.1.0 \
|
|||
# via
|
||||
# -r requirements.in
|
||||
# pip-tools
|
||||
setuptools==73.0.0 \
|
||||
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
|
||||
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
|
||||
setuptools==73.0.1 \
|
||||
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
|
||||
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
|
||||
# via
|
||||
# build
|
||||
# pip-tools
|
||||
|
|
@ -54,9 +54,9 @@ wheel==0.44.0 \
|
|||
# via
|
||||
# -r requirements.in
|
||||
# pip-tools
|
||||
zipp==3.20.0 \
|
||||
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
|
||||
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
|
||||
zipp==3.20.1 \
|
||||
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
|
||||
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
|
||||
# via
|
||||
# -r requirements.in
|
||||
# pip-tools
|
||||
|
|
|
|||
|
|
@ -223,13 +223,13 @@ distro==1.9.0 \
|
|||
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
|
||||
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
|
||||
# via certbot
|
||||
idna==3.7 \
|
||||
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
|
||||
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
|
||||
idna==3.8 \
|
||||
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
|
||||
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
|
||||
# via requests
|
||||
importlib-metadata==8.3.0 \
|
||||
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
|
||||
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
|
||||
importlib-metadata==8.4.0 \
|
||||
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
|
||||
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
|
||||
# via certbot
|
||||
josepy==1.14.0 \
|
||||
--hash=sha256:308b3bf9ce825ad4d4bba76372cf19b5dc1c2ce96a9d298f9642975e64bd13dd \
|
||||
|
|
@ -351,9 +351,9 @@ schedule==1.2.2 \
|
|||
# via importlib-metadata
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
setuptools==73.0.0 \
|
||||
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
|
||||
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
|
||||
setuptools==73.0.1 \
|
||||
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
|
||||
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
|
||||
# via -r requirements.in
|
||||
six==1.16.0 \
|
||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
||||
|
|
@ -363,9 +363,9 @@ urllib3==2.2.2 \
|
|||
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
|
||||
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
|
||||
# via requests
|
||||
zipp==3.20.0 \
|
||||
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
|
||||
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
|
||||
zipp==3.20.1 \
|
||||
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
|
||||
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
|
|
|
|||
|
|
@ -21,18 +21,6 @@ RUN export MAKEFLAGS="-j$(nproc)" && \
|
|||
pip install --no-cache-dir --require-hashes --break-system-packages -r /tmp/requirements-deps.txt && \
|
||||
pip install --no-cache-dir --require-hashes --target deps/python $(for file in $(ls /tmp/req/requirements*.txt) ; do echo "-r ${file}" ; done | xargs)
|
||||
|
||||
# Install node and npm to build vite frontend
|
||||
RUN apk add --no-cache nodejs npm
|
||||
|
||||
COPY src/ui/client ui/client
|
||||
|
||||
# This will build the frontend and add files to the UI folder
|
||||
WORKDIR /usr/share/bunkerweb/ui/client
|
||||
RUN DOCKERFILE=yes python3 build.py
|
||||
|
||||
# We can delete the client folder after building the frontend
|
||||
RUN rm -rf /usr/share/bunkerweb/ui/client
|
||||
|
||||
WORKDIR /usr/share/bunkerweb
|
||||
|
||||
# Copy files
|
||||
|
|
@ -45,10 +33,7 @@ COPY src/common/settings.json settings.json
|
|||
COPY src/common/utils utils
|
||||
COPY src/common/helpers helpers
|
||||
COPY src/VERSION VERSION
|
||||
COPY src/ui/pages ui/pages
|
||||
COPY src/ui/src ui/src
|
||||
COPY src/ui/*.py ui/
|
||||
COPY --chmod=750 src/ui/entrypoint.sh ui/
|
||||
COPY src/ui ui
|
||||
|
||||
FROM python:3.12.5-alpine@sha256:c2f41e6a5a67bc39b95be3988dd19fbd05d1b82375c46d9826c592cca014d4de
|
||||
|
||||
|
|
@ -78,7 +63,8 @@ RUN echo "Docker" > INTEGRATION && \
|
|||
for dir in $(echo "pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
|
||||
chown -R root:ui INTEGRATION /data /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb && \
|
||||
chmod -R 770 /data /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb && \
|
||||
chmod 750 gen/*.py ui/*.py ui/src/*.py helpers/*.sh deps/python/bin/* && \
|
||||
chmod 750 gen/*.py ui/*.sh helpers/*.sh deps/python/bin/* && \
|
||||
find ui -name "*.py" -type f -exec chmod 750 {} \; && \
|
||||
chmod 660 INTEGRATION && \
|
||||
ln -s /proc/1/fd/1 /var/log/bunkerweb/ui-access.log && \
|
||||
ln -s /proc/1/fd/2 /var/log/bunkerweb/ui.log
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ from logging import getLogger
|
|||
from os import sep
|
||||
from pathlib import Path
|
||||
|
||||
from src.config import Config
|
||||
from src.instance import InstancesUtils
|
||||
from src.ui_data import UIData
|
||||
|
||||
from ui_database import UIDatabase
|
||||
from app.models.config import Config
|
||||
from app.models.instance import InstancesUtils
|
||||
from app.models.ui_data import UIData
|
||||
from app.models.ui_database import UIDatabase
|
||||
|
||||
DB = UIDatabase(getLogger("UI"), log=False)
|
||||
DATA = UIData(Path(sep, "var", "tmp", "bunkerweb").joinpath("ui_data.json"))
|
||||
|
|
@ -9,8 +9,7 @@ for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in ((
|
|||
from bcrypt import checkpw
|
||||
from flask_login import AnonymousUserMixin, UserMixin
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
from sqlalchemy import Boolean, DateTime, Column, Identity, Integer, String, ForeignKey, UnicodeText
|
||||
|
||||
from sqlalchemy import TEXT, Boolean, DateTime, Column, Identity, Integer, String, ForeignKey, UnicodeText
|
||||
|
||||
from model import METHODS_ENUM # type: ignore
|
||||
|
||||
|
|
@ -23,9 +22,6 @@ class AnonymousUser(AnonymousUserMixin):
|
|||
password = ""
|
||||
method = "manual"
|
||||
admin = False
|
||||
last_login_at = None
|
||||
last_login_ip = None
|
||||
login_count = 0
|
||||
totp_secret = None
|
||||
creation_date = datetime.now()
|
||||
update_date = datetime.now()
|
||||
|
|
@ -49,11 +45,6 @@ class Users(Base, UserMixin):
|
|||
method = Column(METHODS_ENUM, nullable=False, default="manual")
|
||||
admin = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
# Trackable
|
||||
last_login_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_login_ip = Column(String(39), nullable=True)
|
||||
login_count = Column(Integer, default=0, nullable=False)
|
||||
|
||||
# 2FA
|
||||
totp_secret = Column(String(256), nullable=True)
|
||||
|
||||
|
|
@ -62,6 +53,7 @@ class Users(Base, UserMixin):
|
|||
|
||||
roles = relationship("RolesUsers", back_populates="user", cascade="all")
|
||||
recovery_codes = relationship("UserRecoveryCodes", back_populates="user", cascade="all")
|
||||
sessions = relationship("UserSessions", back_populates="user", cascade="all")
|
||||
list_roles: list[str] = []
|
||||
list_permissions: list[str] = []
|
||||
list_recovery_codes: list[str] = []
|
||||
|
|
@ -120,3 +112,16 @@ class Permissions(Base):
|
|||
name = Column(String(64), primary_key=True)
|
||||
|
||||
roles = relationship("RolesPermissions", back_populates="permission", cascade="all")
|
||||
|
||||
|
||||
class UserSessions(Base):
|
||||
__tablename__ = "bw_ui_user_sessions"
|
||||
|
||||
id = Column(Integer, Identity(start=1, increment=1), primary_key=True)
|
||||
user_name = Column(String(256), ForeignKey("bw_ui_users.username", onupdate="cascade", ondelete="cascade"), nullable=False)
|
||||
ip = Column(String(39), nullable=False)
|
||||
user_agent = Column(TEXT, nullable=False)
|
||||
creation_date = Column(DateTime(timezone=True), nullable=False)
|
||||
last_activity = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
user = relationship("Users", back_populates="sessions")
|
||||
|
|
@ -9,9 +9,9 @@ from passlib.pwd import genword
|
|||
from qrcode import make
|
||||
from qrcode.image.svg import SvgImage
|
||||
|
||||
from models import Users
|
||||
from dependencies import DATA
|
||||
from utils import LIB_DIR, LOGGER, stop
|
||||
from app.models.models import Users
|
||||
from app.dependencies import DATA
|
||||
from app.utils import LIB_DIR, LOGGER, stop
|
||||
|
||||
|
||||
TOTP_SECRETS = getenv("TOTP_SECRETS", "")
|
||||
|
|
@ -54,7 +54,7 @@ class Totp:
|
|||
return self._totp.from_source(totp_secret).pretty_key()
|
||||
|
||||
def generate_recovery_codes(self) -> List[str]:
|
||||
return ["-".join([pwd[i : i + 4] for i in range(0, len(pwd), 4)]) for pwd in genword(length=16, charset="hex", returns=5)] # noqa: E203
|
||||
return ["-".join([pwd[i : i + 4] for i in range(0, len(pwd), 4)]) for pwd in genword(length=16, charset="hex", returns=6)] # noqa: E203
|
||||
|
||||
def verify_recovery_code(self, code: str, user: Users) -> Optional[str]:
|
||||
"""Check if recovery code is valid for user."""
|
||||
|
|
@ -27,3 +27,7 @@ class UIData(dict):
|
|||
def __delitem__(self, key):
|
||||
super().__delitem__(key)
|
||||
self._write_to_file()
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
super().update(*args, **kwargs)
|
||||
self._write_to_file()
|
||||
|
|
@ -19,7 +19,7 @@ from sqlalchemy.exc import IntegrityError
|
|||
from Database import Database # type: ignore
|
||||
from model import Metadata # type: ignore
|
||||
|
||||
from models import Base, Users, Roles, RolesUsers, UserRecoveryCodes, RolesPermissions, Permissions
|
||||
from app.models.models import Base, Permissions, Roles, RolesPermissions, RolesUsers, Users, UserRecoveryCodes, UserSessions
|
||||
|
||||
|
||||
class UIDatabase(Database):
|
||||
|
|
@ -163,9 +163,6 @@ class UIDatabase(Database):
|
|||
"email": ui_user.email,
|
||||
"password": ui_user.password.encode("utf-8"),
|
||||
"method": ui_user.method,
|
||||
"last_login_at": ui_user.last_login_at,
|
||||
"last_login_ip": ui_user.last_login_ip,
|
||||
"login_count": ui_user.login_count,
|
||||
"totp_secret": ui_user.totp_secret,
|
||||
"creation_date": ui_user.creation_date,
|
||||
"update_date": ui_user.update_date,
|
||||
|
|
@ -234,19 +231,31 @@ class UIDatabase(Database):
|
|||
password: bytes,
|
||||
totp_secret: Optional[str],
|
||||
*,
|
||||
old_username: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
totp_recovery_codes: Optional[List[str]] = None,
|
||||
method: str = "manual",
|
||||
) -> str:
|
||||
"""Update ui user."""
|
||||
totp_changed = False
|
||||
old_username = old_username or username
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
user = session.query(Users).filter_by(username=username).first()
|
||||
user = session.query(Users).filter_by(username=old_username).first()
|
||||
if not user:
|
||||
return f"User {username} doesn't exist"
|
||||
return f"User {old_username} doesn't exist"
|
||||
|
||||
if username != old_username:
|
||||
if session.query(Users).with_entities(Users.username).filter_by(username=username).first():
|
||||
return f"User {username} already exists"
|
||||
|
||||
user.username = username
|
||||
|
||||
session.query(RolesUsers).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserRecoveryCodes).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserSessions).filter_by(user_name=old_username).update({"user_name": username})
|
||||
|
||||
totp_changed = user.totp_secret != totp_secret
|
||||
|
||||
|
|
@ -290,16 +299,44 @@ class UIDatabase(Database):
|
|||
|
||||
return ""
|
||||
|
||||
def mark_ui_user_login(self, username: str, date: datetime, ip: str) -> str:
|
||||
def mark_ui_user_login(self, username: str, date: datetime, ip: str, user_agent: str) -> Union[str, int]:
|
||||
"""Mark ui user login."""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
user = session.query(Users).filter_by(username=username).first()
|
||||
if not user:
|
||||
return f"User {username} doesn't exist"
|
||||
|
||||
user.last_login_at = date
|
||||
user.last_login_ip = ip
|
||||
user.login_count += 1
|
||||
user_session = UserSessions(
|
||||
user_name=username,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
creation_date=date,
|
||||
last_activity=date,
|
||||
)
|
||||
session.add(user_session)
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
session.refresh(user_session)
|
||||
session_id = user_session.id
|
||||
return session_id
|
||||
except BaseException as e:
|
||||
return str(e)
|
||||
|
||||
def mark_ui_user_access(self, session_id: int, date: datetime) -> str:
|
||||
"""Mark ui user access."""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
user_session = session.query(UserSessions).filter_by(id=session_id).first()
|
||||
if not user_session:
|
||||
return f"Session {session_id} doesn't exist"
|
||||
|
||||
user_session.last_activity = date
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
|
|
@ -441,3 +478,34 @@ class UIDatabase(Database):
|
|||
return str(e)
|
||||
|
||||
return ""
|
||||
|
||||
def get_ui_user_last_session(self, username: str) -> Optional[UserSessions]:
|
||||
"""Get ui user last session."""
|
||||
with self._db_session() as session:
|
||||
return session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).first()
|
||||
|
||||
def get_ui_user_sessions(self, username: str) -> List[UserSessions]:
|
||||
"""Get ui user sessions."""
|
||||
with self._db_session() as session:
|
||||
return session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).limit(10).all()
|
||||
|
||||
def delete_ui_user_old_sessions(self, username: str) -> str:
|
||||
"""Delete ui user old sessions."""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
user = session.query(Users).filter_by(username=username).first()
|
||||
if not user:
|
||||
return f"User {username} doesn't exist"
|
||||
|
||||
sessions_to_delete = session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).offset(1).all()
|
||||
for session_to_delete in sessions_to_delete:
|
||||
session.delete(session_to_delete)
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
return str(e)
|
||||
|
||||
return ""
|
||||
0
src/ui/app/routes/__init__.py
Normal file
|
|
@ -1,4 +1,3 @@
|
|||
from base64 import b64encode
|
||||
from datetime import datetime
|
||||
from json import dumps, loads as json_loads
|
||||
from math import floor
|
||||
|
|
@ -8,12 +7,10 @@ from flask import Blueprint, flash, redirect, render_template, request, url_for
|
|||
from flask_login import login_required
|
||||
from redis import Redis, Sentinel
|
||||
|
||||
from builder.bans import bans_builder # type: ignore
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
|
||||
from app.utils import LOGGER
|
||||
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
|
||||
from utils import LOGGER
|
||||
|
||||
from pages.utils import get_remain, handle_error, verify_data_in_form
|
||||
from app.routes.utils import get_remain, handle_error, verify_data_in_form
|
||||
|
||||
bans = Blueprint("bans", __name__)
|
||||
|
||||
|
|
@ -218,5 +215,6 @@ def bans_page():
|
|||
ban["ban_end_date"] = datetime.fromtimestamp(floor(timestamp_now + exp)).strftime("%Y/%m/%d at %H:%M:%S %Z")
|
||||
reasons.add(ban["reason"])
|
||||
|
||||
builder = bans_builder(bans, list(reasons), list(remains))
|
||||
return render_template("bans.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = bans_builder(bans, list(reasons), list(remains))
|
||||
# return render_template("bans.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("bans.html") # TODO
|
||||
|
|
@ -3,8 +3,8 @@ from os.path import join, sep
|
|||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from dependencies import BW_CONFIG, DB
|
||||
from utils import path_to_dict
|
||||
from app.dependencies import BW_CONFIG, DB
|
||||
from app.utils import path_to_dict
|
||||
|
||||
|
||||
cache = Blueprint("cache", __name__)
|
||||
|
|
@ -5,10 +5,10 @@ from bs4 import BeautifulSoup
|
|||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import LOGGER, PLUGIN_NAME_RX, path_to_dict
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.utils import LOGGER, PLUGIN_NAME_RX, path_to_dict
|
||||
|
||||
from pages.utils import handle_error, verify_data_in_form
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
||||
|
||||
configs = Blueprint("configs", __name__)
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
from base64 import b64encode
|
||||
from contextlib import suppress
|
||||
from json import dumps
|
||||
from threading import Thread
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.global_config import global_config_builder # type: ignore
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
|
||||
from pages.utils import handle_error, manage_bunkerweb, wait_applying
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, wait_applying
|
||||
|
||||
|
||||
global_config = Blueprint("global_config", __name__)
|
||||
|
|
@ -83,7 +79,8 @@ def global_config_page():
|
|||
)
|
||||
)
|
||||
|
||||
global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
builder = global_config_builder({}, plugins, global_config)
|
||||
return render_template("global-config.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
# plugins = BW_CONFIG.get_plugins()
|
||||
# builder = global_config_builder({}, plugins, global_config)
|
||||
# return render_template("global-config.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("global-config.html") # TODO
|
||||
80
src/ui/app/routes/home.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS # , DB
|
||||
|
||||
home = Blueprint("home", __name__)
|
||||
|
||||
|
||||
@home.route("/home")
|
||||
@login_required
|
||||
def home_page():
|
||||
"""
|
||||
It returns the home page
|
||||
:return: The home.html template is being rendered with the following variables:
|
||||
check_version: a boolean indicating whether the local version is the same as the remote version
|
||||
remote_version: the remote version
|
||||
version: the local version
|
||||
instances_number: the number of instances
|
||||
services_number: the number of services
|
||||
posts: a list of posts
|
||||
"""
|
||||
# try:
|
||||
# r = get("https://github.com/bunkerity/bunkerweb/releases/latest", allow_redirects=True, timeout=5)
|
||||
# r.raise_for_status()
|
||||
# except BaseException:
|
||||
# r = None
|
||||
# remote_version = None
|
||||
|
||||
# if r and r.status_code == 200:
|
||||
# remote_version = basename(r.url).strip().replace("v", "")
|
||||
|
||||
config = BW_CONFIG.get_config(with_drafts=True, filtered_settings=("SERVER_NAME",))
|
||||
instances = BW_INSTANCES_UTILS.get_instances()
|
||||
|
||||
instance_health_count = 0
|
||||
|
||||
for instance in instances:
|
||||
if instance.status == "up":
|
||||
instance_health_count += 1
|
||||
|
||||
services = 0
|
||||
services_scheduler_count = 0
|
||||
services_ui_count = 0
|
||||
services_autoconf_count = 0
|
||||
|
||||
for service in config["SERVER_NAME"]["value"].split(" "):
|
||||
service_method = config.get(f"{service}_SERVER_NAME", {"method": "scheduler"})["method"]
|
||||
|
||||
if service_method == "scheduler":
|
||||
services_scheduler_count += 1
|
||||
elif service_method == "ui":
|
||||
services_ui_count += 1
|
||||
elif service_method == "autoconf":
|
||||
services_autoconf_count += 1
|
||||
services += 1
|
||||
|
||||
# metadata = DB.get_metadata()
|
||||
|
||||
# data = {
|
||||
# "check_version": not remote_version or get_version() == remote_version,
|
||||
# "remote_version": remote_version,
|
||||
# "version": metadata["version"],
|
||||
# "instances_number": len(instances),
|
||||
# "services_number": services,
|
||||
# "instance_health_count": instance_health_count,
|
||||
# "services_scheduler_count": services_scheduler_count,
|
||||
# "services_ui_count": services_ui_count,
|
||||
# "services_autoconf_count": services_autoconf_count,
|
||||
# "is_pro_version": metadata["is_pro"],
|
||||
# "pro_status": metadata["pro_status"],
|
||||
# "pro_services": metadata["pro_services"],
|
||||
# "pro_overlapped": metadata["pro_overlapped"],
|
||||
# "plugins_number": len(BW_CONFIG.get_plugins()),
|
||||
# "plugins_errors": DB.get_plugins_errors(),
|
||||
# }
|
||||
|
||||
# builder = home_builder(data)
|
||||
# return render_template("home.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("home.html") # TODO
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Literal
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.instances import instances_builder # type: ignore
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
|
||||
from pages.utils import handle_error, manage_bunkerweb, verify_data_in_form
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, verify_data_in_form
|
||||
|
||||
|
||||
instances = Blueprint("instances", __name__)
|
||||
|
|
@ -41,8 +37,9 @@ def instances_page():
|
|||
instances_methods.add(instance.method)
|
||||
instances_healths.add(instance.status)
|
||||
|
||||
builder = instances_builder(instances, list(instances_types), list(instances_methods), list(instances_healths))
|
||||
return render_template("instances.html", title="Instances", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = instances_builder(instances, list(instances_types), list(instances_methods), list(instances_healths))
|
||||
# return render_template("instances.html", title="Instances", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("instances.html") # TODO
|
||||
|
||||
|
||||
@instances.route("/instances/new", methods=["PUT"])
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
from base64 import b64encode
|
||||
from io import BytesIO
|
||||
from json import dumps
|
||||
|
||||
from flask import Blueprint, jsonify, render_template, request, send_file
|
||||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from builder.jobs import jobs_builder # type: ignore
|
||||
|
||||
from dependencies import DB
|
||||
from app.dependencies import DB
|
||||
|
||||
jobs = Blueprint("jobs", __name__)
|
||||
|
||||
|
|
@ -16,8 +12,9 @@ jobs = Blueprint("jobs", __name__)
|
|||
@jobs.route("/jobs", methods=["GET"])
|
||||
@login_required
|
||||
def jobs_page():
|
||||
builder = jobs_builder(DB.get_jobs())
|
||||
return render_template("jobs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = jobs_builder(DB.get_jobs())
|
||||
# return render_template("jobs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("jobs.html") # TODO
|
||||
|
||||
|
||||
@jobs.route("/jobs/download", methods=["GET"])
|
||||
|
|
@ -3,8 +3,8 @@ from datetime import datetime
|
|||
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user, login_user
|
||||
|
||||
from dependencies import DB
|
||||
from utils import LOGGER
|
||||
from app.dependencies import DB
|
||||
from app.utils import LOGGER
|
||||
|
||||
login = Blueprint("login", __name__)
|
||||
|
||||
|
|
@ -19,28 +19,31 @@ def login_page():
|
|||
|
||||
fail = False
|
||||
if request.method == "POST" and "username" in request.form and "password" in request.form:
|
||||
LOGGER.debug(request.form)
|
||||
LOGGER.warning(f"Login attempt from {request.remote_addr} with username \"{request.form['username']}\"")
|
||||
|
||||
ui_user = DB.get_ui_user(username=request.form["username"])
|
||||
if ui_user and ui_user.username == request.form["username"] and ui_user.check_password(request.form["password"]):
|
||||
# log the user in
|
||||
session["user_agent"] = request.headers.get("User-Agent")
|
||||
session["totp_validated"] = False
|
||||
session["flash_messages"] = []
|
||||
|
||||
ui_user.last_login_at = datetime.now()
|
||||
ui_user.last_login_ip = request.remote_addr
|
||||
ui_user.login_count += 1
|
||||
ret = DB.mark_ui_user_login(
|
||||
ui_user.username,
|
||||
datetime.now().astimezone(),
|
||||
request.remote_addr,
|
||||
request.headers.get("User-Agent"),
|
||||
)
|
||||
if isinstance(ret, str):
|
||||
LOGGER.error(f"Couldn't mark the user login: {ret}")
|
||||
else:
|
||||
session["session_id"] = ret
|
||||
|
||||
DB.mark_ui_user_login(ui_user.username, ui_user.last_login_at, ui_user.last_login_ip)
|
||||
|
||||
if not login_user(ui_user, remember=request.form.get("remember") == "on"):
|
||||
if not login_user(ui_user, remember=request.form.get("remember-me") == "on"):
|
||||
flash("Couldn't log you in, please try again", "error")
|
||||
return (render_template("login.html", error="Couldn't log you in, please try again"),)
|
||||
|
||||
LOGGER.info(
|
||||
f"User {ui_user.username} logged in successfully for the {str(ui_user.login_count) + ('th' if 10 <= ui_user.login_count % 100 <= 20 else {1: 'st', 2: 'nd', 3: 'rd'}.get(ui_user.login_count % 10, 'th'))} time"
|
||||
+ (" with remember me" if request.form.get("remember") == "on" else "")
|
||||
)
|
||||
LOGGER.info(f"User {ui_user.username} logged in successfully" + (" with remember me" if request.form.get("remember-me") == "on" else ""))
|
||||
|
||||
# redirect him to the page he originally wanted or to the home page
|
||||
return redirect(url_for("loading", next=request.form.get("next") or url_for("home.home_page")))
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
from os.path import isabs, sep
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -7,9 +5,7 @@ from flask import Blueprint, Response, render_template, request
|
|||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from builder.logs import logs_builder # type: ignore
|
||||
|
||||
from pages.utils import error_message
|
||||
from app.routes.utils import error_message
|
||||
|
||||
|
||||
logs = Blueprint("logs", __name__)
|
||||
|
|
@ -34,10 +30,11 @@ def logs_page():
|
|||
if isabs(current_file) or ".." in current_file:
|
||||
return error_message("Invalid file path", 400)
|
||||
|
||||
raw_logs = ""
|
||||
if current_file:
|
||||
with logs_path.joinpath(current_file).open(encoding="utf-8") as f:
|
||||
raw_logs = f.read()
|
||||
# raw_logs = ""
|
||||
# if current_file:
|
||||
# with logs_path.joinpath(current_file).open(encoding="utf-8") as f:
|
||||
# raw_logs = f.read()
|
||||
|
||||
builder = logs_builder(files, current_file, raw_logs)
|
||||
return render_template("logs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = logs_builder(files, current_file, raw_logs)
|
||||
# return render_template("logs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("logs.html") # TODO
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.advanced_mode import advanced_mode_builder # type: ignore
|
||||
from builder.easy_mode import easy_mode_builder # type: ignore
|
||||
from builder.raw_mode import raw_mode_builder # type: ignore
|
||||
from app.dependencies import DB
|
||||
|
||||
from dependencies import BW_CONFIG, DB
|
||||
|
||||
from pages.utils import get_service_data, handle_error, update_service
|
||||
from app.routes.utils import get_service_data, handle_error, update_service
|
||||
|
||||
modes = Blueprint("modes", __name__)
|
||||
|
||||
|
|
@ -36,7 +29,7 @@ def services_modes():
|
|||
if not request.args.get("mode"):
|
||||
return handle_error("Mode type is missing to access /modes.", "services")
|
||||
|
||||
mode = request.args.get("mode")
|
||||
# mode = request.args.get("mode")
|
||||
service_name = request.args.get("service_name")
|
||||
total_config = DB.get_config(methods=True, with_drafts=True)
|
||||
service_names = total_config["SERVER_NAME"]["value"].split(" ")
|
||||
|
|
@ -44,17 +37,18 @@ def services_modes():
|
|||
if service_name and service_name not in service_names:
|
||||
return handle_error("Service name not found to access advanced mode.", "services")
|
||||
|
||||
global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
# global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
# plugins = BW_CONFIG.get_plugins()
|
||||
|
||||
builder = None
|
||||
templates_db = DB.get_templates()
|
||||
# builder = None
|
||||
# templates_db = DB.get_templates()
|
||||
|
||||
if mode == "raw":
|
||||
builder = raw_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
elif mode == "advanced":
|
||||
builder = advanced_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
elif mode == "easy":
|
||||
builder = easy_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
# if mode == "raw":
|
||||
# builder = raw_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
# elif mode == "advanced":
|
||||
# builder = advanced_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
# elif mode == "easy":
|
||||
# builder = easy_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
|
||||
return render_template("modes.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# return render_template("modes.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("modes.html") # TODO
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
from base64 import b64encode
|
||||
from copy import deepcopy
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from io import BytesIO
|
||||
from json import JSONDecodeError, dumps, loads as json_loads
|
||||
from json import JSONDecodeError, loads as json_loads
|
||||
from os import listdir
|
||||
from os.path import basename, dirname, isabs, join, sep
|
||||
from pathlib import Path
|
||||
|
|
@ -22,12 +21,10 @@ from werkzeug.utils import secure_filename
|
|||
|
||||
from common_utils import bytes_hash # type: ignore
|
||||
|
||||
from builder.plugins import plugins_builder # type: ignore
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
from app.utils import LOGGER, PLUGIN_NAME_RX, TMP_DIR
|
||||
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
from utils import LOGGER, PLUGIN_NAME_RX, TMP_DIR
|
||||
|
||||
from pages.utils import PLUGIN_ID_RX, PLUGIN_KEYS, error_message, handle_error, verify_data_in_form, wait_applying
|
||||
from app.routes.utils import PLUGIN_ID_RX, PLUGIN_KEYS, error_message, handle_error, verify_data_in_form, wait_applying
|
||||
|
||||
|
||||
plugins = Blueprint("plugins", __name__)
|
||||
|
|
@ -345,8 +342,9 @@ def plugins_page():
|
|||
if tmp_ui_path.is_dir():
|
||||
rmtree(tmp_ui_path, ignore_errors=True)
|
||||
|
||||
builder = plugins_builder(DB.get_plugins())
|
||||
return render_template("plugins.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = plugins_builder(DB.get_plugins())
|
||||
# return render_template("plugins.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("plugins.html") # TODO
|
||||
|
||||
|
||||
@plugins.route("/plugins/delete", methods=["POST"])
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for, session
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for, session
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
from user_agents import parse
|
||||
|
||||
from builder.profile import profile_builder # type: ignore
|
||||
from app.models.totp import totp as TOTP
|
||||
|
||||
from src.totp import totp as TOTP
|
||||
from app.dependencies import DB
|
||||
from app.utils import USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from dependencies import DB
|
||||
from utils import USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from pages.utils import handle_error, verify_data_in_form
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
||||
profile = Blueprint("profile", __name__)
|
||||
|
||||
|
|
@ -23,19 +20,28 @@ def profile_page():
|
|||
session["tmp_totp_secret"] = TOTP.generate_totp_secret()
|
||||
totp_qr_image = TOTP.generate_qrcode(current_user.get_id(), session["tmp_totp_secret"])
|
||||
|
||||
builder = profile_builder(
|
||||
current_user if current_user.is_authenticated else None,
|
||||
{
|
||||
"is_totp": bool(current_user.totp_secret),
|
||||
"totp_image": totp_qr_image,
|
||||
"totp_recovery_codes": current_user.list_recovery_codes,
|
||||
"is_recovery_refreshed": False,
|
||||
"totp_secret": TOTP.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
|
||||
},
|
||||
)
|
||||
last_sessions = []
|
||||
for db_session in DB.get_ui_user_sessions(current_user.username):
|
||||
ua_data = parse(db_session.user_agent)
|
||||
last_sessions.append(
|
||||
{
|
||||
"browser": ua_data.get_browser(),
|
||||
"os": ua_data.get_os(),
|
||||
"ip": db_session.ip,
|
||||
"creation_date": db_session.creation_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
|
||||
"last_activity": db_session.last_activity.strftime("%Y-%m-%d %H:%M:%S %Z"),
|
||||
}
|
||||
)
|
||||
|
||||
# TODO: Show user backup codes after TOTP refresh + add refresh feature
|
||||
return render_template("profile.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template(
|
||||
"profile.html",
|
||||
is_totp=bool(current_user.totp_secret),
|
||||
totp_qr_image=totp_qr_image,
|
||||
totp_recovery_codes=session.pop("decrypted_recovery_codes", current_user.list_recovery_codes),
|
||||
is_recovery_refreshed=session.pop("totp_refreshed", False),
|
||||
totp_secret=TOTP.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
|
||||
last_sessions=last_sessions,
|
||||
)
|
||||
|
||||
|
||||
@profile.route("/profile/totp-refresh", methods=["POST"])
|
||||
|
|
@ -47,9 +53,9 @@ def totp_refresh():
|
|||
if not bool(current_user.totp_secret):
|
||||
return handle_error("Two-factor authentication is not enabled.", "profile")
|
||||
|
||||
verify_data_in_form(data={"curr_password": None}, err_message="Missing current password parameter on /profile/totp-refresh.", redirect_url="profile")
|
||||
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/totp-refresh.", redirect_url="profile")
|
||||
|
||||
if not current_user.check_password(request.form["curr_password"]):
|
||||
if not current_user.check_password(request.form["password"]):
|
||||
return handle_error("The current password is incorrect.", "profile")
|
||||
|
||||
totp_recovery_codes = TOTP.generate_recovery_codes()
|
||||
|
|
@ -58,18 +64,11 @@ def totp_refresh():
|
|||
if ret:
|
||||
return handle_error(f"Couldn't refresh the recovery codes in the database: {ret}", "profile")
|
||||
|
||||
session["totp_refreshed"] = True
|
||||
session["decrypted_recovery_codes"] = totp_recovery_codes
|
||||
|
||||
flash("The recovery codes have been successfully refreshed. The old ones are no longer valid.")
|
||||
builder = profile_builder(
|
||||
current_user if current_user.is_authenticated else None,
|
||||
{
|
||||
"is_totp": True,
|
||||
"totp_image": "",
|
||||
"totp_recovery_codes": totp_recovery_codes,
|
||||
"is_recovery_refreshed": True,
|
||||
"totp_secret": TOTP.get_totp_pretty_key(current_user.totp_secret),
|
||||
},
|
||||
)
|
||||
return render_template("profile.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return redirect(url_for("profile.profile_page"))
|
||||
|
||||
|
||||
@profile.route("/profile/totp-disable", methods=["POST"])
|
||||
|
|
@ -81,9 +80,9 @@ def totp_disable():
|
|||
if not bool(current_user.totp_secret):
|
||||
return handle_error("Two-factor authentication is not enabled.", "profile")
|
||||
|
||||
verify_data_in_form(data={"curr_password": None}, err_message="Missing current password parameter on /profile/totp-disable.", redirect_url="profile")
|
||||
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/totp-disable.", redirect_url="profile")
|
||||
|
||||
if not current_user.check_password(request.form["curr_password"]):
|
||||
if not current_user.check_password(request.form["password"]):
|
||||
return handle_error("The current password is incorrect.", "profile")
|
||||
|
||||
verify_data_in_form(data={"totp_token": None}, err_message="Missing totp token parameter on /profile/totp-enable.", redirect_url="profile")
|
||||
|
|
@ -93,14 +92,14 @@ def totp_disable():
|
|||
):
|
||||
return handle_error("The totp token is invalid.", "profile")
|
||||
|
||||
ret = DB.update_ui_user(current_user.get_id(), current_user.password, None, method=current_user.method)
|
||||
ret = DB.update_ui_user(current_user.get_id(), current_user.password.encode("utf-8"), None, method=current_user.method)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't disable the two-factor authentication in the database: {ret}", "profile")
|
||||
|
||||
session["totp_validated"] = False
|
||||
|
||||
flash("The two-factor authentication has been successfully disabled.")
|
||||
return redirect(url_for("profile.profile_page", message="Disabling two-factor authentication."))
|
||||
return redirect(url_for("profile.profile_page"))
|
||||
|
||||
|
||||
@profile.route("/profile/totp-enable", methods=["POST"])
|
||||
|
|
@ -112,10 +111,10 @@ def totp_enable():
|
|||
if bool(current_user.totp_secret):
|
||||
return handle_error("Two-factor authentication is already enabled.", "profile")
|
||||
|
||||
verify_data_in_form(data={"curr_password": None}, err_message="Missing current password parameter on /profile/totp-enable.", redirect_url="profile")
|
||||
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/totp-enable.", redirect_url="profile")
|
||||
verify_data_in_form(data={"totp_token": None}, err_message="Missing totp token parameter on /profile/totp-enable.", redirect_url="profile")
|
||||
|
||||
if not current_user.check_password(request.form["curr_password"]):
|
||||
if not current_user.check_password(request.form["password"]):
|
||||
return handle_error("The current password is incorrect.", "profile")
|
||||
|
||||
if not TOTP.verify_totp(request.form["totp_token"], totp_secret=session.get("tmp_totp_secret", ""), user=current_user) and not TOTP.verify_recovery_code(
|
||||
|
|
@ -128,7 +127,7 @@ def totp_enable():
|
|||
|
||||
ret = DB.update_ui_user(
|
||||
current_user.get_id(),
|
||||
current_user.password,
|
||||
current_user.password.encode("utf-8"),
|
||||
totp_secret,
|
||||
totp_recovery_codes=totp_recovery_codes,
|
||||
method=current_user.method,
|
||||
|
|
@ -137,59 +136,51 @@ def totp_enable():
|
|||
return handle_error(f"Couldn't enable the two-factor authentication in the database: {ret}", "profile")
|
||||
|
||||
session["totp_validated"] = True
|
||||
session["totp_refreshed"] = True
|
||||
session["decrypted_recovery_codes"] = totp_recovery_codes
|
||||
|
||||
flash("The two-factor authentication has been successfully enabled.")
|
||||
builder = profile_builder(
|
||||
current_user if current_user.is_authenticated else None,
|
||||
{
|
||||
"is_totp": True,
|
||||
"totp_image": "",
|
||||
"totp_recovery_codes": totp_recovery_codes,
|
||||
"is_recovery_refreshed": True,
|
||||
"totp_secret": TOTP.get_totp_pretty_key(totp_secret),
|
||||
},
|
||||
)
|
||||
return render_template("profile.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return redirect(url_for("profile.profile_page"))
|
||||
|
||||
|
||||
@profile.route("/profile/edit/<string:field>", methods=["POST"])
|
||||
@profile.route("/profile/edit", methods=["POST"])
|
||||
@login_required
|
||||
def edit_profile(field: str):
|
||||
if field not in ("email", "password"):
|
||||
return Response(status=404)
|
||||
|
||||
def edit_profile():
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "profile")
|
||||
|
||||
verify_data_in_form(data={"curr_password": None}, err_message=f"Missing current password parameter on /profile/edit/{field}.", redirect_url="profile")
|
||||
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/edit.", redirect_url="profile")
|
||||
|
||||
if not current_user.check_password(request.form["curr_password"]):
|
||||
if not current_user.check_password(request.form["password"]):
|
||||
return handle_error("The current password is incorrect.", "profile")
|
||||
|
||||
user_data = {
|
||||
"username": current_user.get_id(),
|
||||
"password": current_user.password,
|
||||
"password": current_user.password.encode("utf-8"),
|
||||
"email": current_user.email,
|
||||
"totp_secret": current_user.totp_secret,
|
||||
"method": current_user.method,
|
||||
}
|
||||
|
||||
if field == "email":
|
||||
verify_data_in_form(data={"email": None}, err_message="Missing email parameter on /profile/edit/email.", redirect_url="profile")
|
||||
if "username" in request.form:
|
||||
verify_data_in_form(data={"email": None}, err_message="Missing email parameter on /profile/edit.", redirect_url="profile")
|
||||
|
||||
if len(request.form["email"]) > 256:
|
||||
return handle_error("The email is too long. It must be less than 256 characters.", "profile")
|
||||
if request.form["email"] and request.form["email"] != current_user.email:
|
||||
if len(request.form["email"]) > 256:
|
||||
return handle_error("The email is too long. It must be less than 256 characters.", "profile")
|
||||
user_data["email"] = request.form["email"] or None
|
||||
|
||||
user_data["email"] = request.form["email"]
|
||||
elif field == "password":
|
||||
verify_data_in_form(
|
||||
data={"new_password": None, "new_password_check": None},
|
||||
err_message="Missing new password or confirm password parameter on /profile/edit/password.",
|
||||
redirect_url="profile",
|
||||
)
|
||||
if request.form["username"] and request.form["username"] != current_user.get_id():
|
||||
if len(request.form["username"]) > 256:
|
||||
return handle_error("The username is too long. It must be less than 256 characters.", "profile")
|
||||
user_data["username"] = request.form["username"]
|
||||
|
||||
if request.form["email"] == (current_user.email or "") and request.form["username"] == current_user.get_id():
|
||||
return handle_error("The username and email are the same as the current ones.", "profile")
|
||||
elif "new_password" in request.form:
|
||||
verify_data_in_form(
|
||||
data={"new_password_confirm": None},
|
||||
err_message="Missing new password confirm parameter on /profile/edit/password.",
|
||||
err_message="Missing new password confirm parameter on /profile/edit.",
|
||||
redirect_url="profile",
|
||||
)
|
||||
|
||||
|
|
@ -200,16 +191,39 @@ def edit_profile(field: str):
|
|||
"The new password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-).",
|
||||
"profile",
|
||||
)
|
||||
elif current_user.check_password(request.form["new_password"]):
|
||||
return handle_error("The new password is the same as the current one.", "profile")
|
||||
|
||||
user_data["password"] = gen_password_hash(request.form["new_password"])
|
||||
else:
|
||||
return handle_error("No fields were updated.", "profile")
|
||||
|
||||
ret = DB.update_ui_user(**user_data)
|
||||
ret = DB.update_ui_user(**user_data, old_username=current_user.get_id())
|
||||
if ret:
|
||||
return handle_error(f"Couldn't update the admin user in the database: {ret}", "profile")
|
||||
|
||||
if field == "password":
|
||||
if "new_password" in request.form:
|
||||
session.clear()
|
||||
logout_user()
|
||||
|
||||
flash(f"The {field} has been successfully updated.")
|
||||
return redirect(url_for("profile.profile_page" if field == "email" else "login.login_page"))
|
||||
flash("The profile has been successfully updated.")
|
||||
return redirect(url_for("profile.profile_page"))
|
||||
|
||||
|
||||
@profile.route("/profile/wipe-old-sessions", methods=["POST"])
|
||||
@login_required
|
||||
def wipe_old_sessions():
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "profile")
|
||||
|
||||
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/wipe-old-sessions.", redirect_url="profile")
|
||||
|
||||
if not current_user.check_password(request.form["password"]):
|
||||
return handle_error("The current password is incorrect.", "profile")
|
||||
|
||||
ret = DB.delete_ui_user_old_sessions(current_user.username)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't wipe the old sessions in the database: {ret}", "profile")
|
||||
|
||||
flash("The old sessions have been successfully wiped.")
|
||||
return redirect(url_for("profile.profile_page") + "#sessions")
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
from math import floor
|
||||
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from dependencies import BW_INSTANCES_UTILS
|
||||
|
||||
from builder.reports import reports_builder # type: ignore
|
||||
from app.dependencies import BW_INSTANCES_UTILS
|
||||
|
||||
reports = Blueprint("reports", __name__)
|
||||
|
||||
|
|
@ -35,5 +31,6 @@ def reports_page():
|
|||
methods.add(report["method"])
|
||||
codes.add(report["code"])
|
||||
|
||||
builder = reports_builder(reports_items, list(reasons), list(countries), list(methods), list(codes))
|
||||
return render_template("reports.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = reports_builder(reports_items, list(reasons), list(countries), list(methods), list(codes))
|
||||
# return render_template("reports.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("reports.html")
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.services import services_builder # type: ignore
|
||||
from app.dependencies import DB
|
||||
|
||||
from dependencies import DB
|
||||
|
||||
from pages.utils import get_service_data, handle_error, update_service
|
||||
from app.routes.utils import get_service_data, handle_error, update_service
|
||||
|
||||
services = Blueprint("services", __name__)
|
||||
|
||||
|
|
@ -59,5 +54,6 @@ def services_page():
|
|||
|
||||
services.sort(key=lambda x: x["SERVER_NAME"]["value"])
|
||||
|
||||
builder = services_builder(services)
|
||||
return render_template("services.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
# builder = services_builder(services)
|
||||
# return render_template("services.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("services.html")
|
||||
|
|
@ -6,10 +6,10 @@ from time import time
|
|||
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import USER_PASSWORD_RX, gen_password_hash
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.utils import USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from pages.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
|
||||
from app.routes.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
|
||||
|
||||
setup = Blueprint("setup", __name__)
|
||||
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
from flask import Blueprint, redirect, render_template, request, session, url_for
|
||||
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from src.totp import totp as TOTP
|
||||
|
||||
from dependencies import DB
|
||||
|
||||
from pages.utils import handle_error, verify_data_in_form
|
||||
from app.dependencies import DB
|
||||
from app.models.totp import totp as TOTP
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
||||
totp = Blueprint("totp", __name__)
|
||||
|
||||
|
|
@ -20,6 +18,7 @@ def totp_page():
|
|||
recovery_code = TOTP.verify_recovery_code(request.form["totp_token"], user=current_user)
|
||||
if not recovery_code:
|
||||
return handle_error("The token is invalid.", "totp")
|
||||
flash(f"You've used one of your recovery codes. You have {len(current_user.list_recovery_codes)} left.")
|
||||
DB.use_ui_user_recovery_code(current_user.get_id(), recovery_code)
|
||||
|
||||
session["totp_validated"] = True
|
||||
|
|
@ -6,14 +6,14 @@ from threading import Thread
|
|||
from time import sleep, time
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from flask import Response, flash, redirect, request, url_for
|
||||
from flask import Response, flash, redirect, request, session, url_for
|
||||
from qrcode.main import QRCode
|
||||
from regex import compile as re_compile
|
||||
|
||||
from src.instance import Instance
|
||||
from app.models.instance import Instance
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import LOGGER
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.utils import LOGGER
|
||||
|
||||
|
||||
LOG_RX = re_compile(r"^(?P<date>\d+/\d+/\d+\s\d+:\d+:\d+)\s\[(?P<level>[a-z]+)\]\s\d+#\d+:\s(?P<message>[^\n]+)$")
|
||||
|
|
@ -111,6 +111,9 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
|
|||
else:
|
||||
flash(f["content"])
|
||||
|
||||
if "flash_messages" in session:
|
||||
session["flash_messages"].append((f["content"], f["type"], datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")))
|
||||
|
||||
DATA["TO_FLASH"] = []
|
||||
|
||||
DATA["RELOADING"] = False
|
||||
24887
src/ui/app/static/css/core.css
Normal file
194
src/ui/app/static/css/main.css
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
.menu .app-brand.main {
|
||||
height: 64px;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-brand-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-brand-logo.main {
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-brand-logo.main svg {
|
||||
width: 50%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.app-brand-text.main {
|
||||
font-size: 1.75rem;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* ! For .layout-navbar-fixed added fix padding top to .layout-page */
|
||||
/* Detached navbar */
|
||||
.layout-navbar-fixed
|
||||
.layout-wrapper:not(.layout-horizontal):not(.layout-without-menu)
|
||||
.layout-page {
|
||||
padding-top: 74px !important;
|
||||
}
|
||||
/* Default navbar */
|
||||
.layout-navbar-fixed .layout-wrapper:not(.layout-without-menu) .layout-page {
|
||||
padding-top: 64px !important;
|
||||
}
|
||||
.docs-page
|
||||
.layout-navbar-fixed.layout-wrapper:not(.layout-without-menu)
|
||||
.layout-page,
|
||||
.docs-page
|
||||
.layout-menu-fixed.layout-wrapper:not(.layout-without-menu)
|
||||
.layout-page {
|
||||
padding-top: 62px !important;
|
||||
}
|
||||
|
||||
/* Navbar page z-index issue solution */
|
||||
.content-wrapper .navbar {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Content
|
||||
******************************************************************************/
|
||||
|
||||
.main-blocks > * {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.main-inline-spacing > * {
|
||||
margin: 1rem 0.375rem 0 0 !important;
|
||||
}
|
||||
|
||||
/* ? .main-vertical-spacing class is used to have vertical margins between elements. To remove margin-top from the first-child, use .main-only-element class with .main-vertical-spacing class. For example, we have used this class in forms-input-groups.html file. */
|
||||
.main-vertical-spacing > * {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.main-vertical-spacing.main-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.main-vertical-spacing-lg > * {
|
||||
margin-top: 1.875rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.main-vertical-spacing-lg.main-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.main-vertical-spacing-xl > * {
|
||||
margin-top: 5rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.main-vertical-spacing-xl.main-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.rtl-only {
|
||||
display: none !important;
|
||||
text-align: left !important;
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
[dir="rtl"] .rtl-only {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Dropdown buttons going out of small screens */
|
||||
@media (max-width: 576px) {
|
||||
#dropdown-variation-main .btn-group .text-truncate {
|
||||
width: 231px;
|
||||
position: relative;
|
||||
}
|
||||
#dropdown-variation-main .btn-group .text-truncate::after {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
right: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Layout main
|
||||
******************************************************************************/
|
||||
|
||||
.layout-main-wrapper {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.layout-main-placeholder img {
|
||||
width: 900px;
|
||||
}
|
||||
.layout-main-info {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Custom
|
||||
******************************************************************************/
|
||||
|
||||
.badge-dot {
|
||||
padding: 0.35rem;
|
||||
font-size: 0.6rem;
|
||||
animation: pulsate 1.7s infinite;
|
||||
}
|
||||
|
||||
.badge-dot-text {
|
||||
font-size: 0.6rem;
|
||||
animation: pulsate 1.7s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulsate {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.w-30 {
|
||||
width: 30% !important;
|
||||
}
|
||||
|
||||
.badge-center-sm {
|
||||
padding: 2.5px;
|
||||
line-height: 1.2;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.badge-center-sm i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.bg-bw-green {
|
||||
background-color: #2eac68;
|
||||
}
|
||||
31
src/ui/app/static/css/pages/loading.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
.img-fluid {
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.layout-main-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layout-main-info {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.layout-main-info h2 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@keyframes pulseAnimation {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.pulsating {
|
||||
animation: pulseAnimation 1.5s infinite ease-in-out;
|
||||
}
|
||||
98
src/ui/app/static/css/pages/login.css
Normal file
1003
src/ui/app/static/css/theme-default.css
Normal file
63
src/ui/app/static/fonts/Public_sans.css
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/300-italic.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/500-italic.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/600-italic.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/700-italic.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/300-normal.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/400-normal.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/500-normal.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/600-normal.ttf), format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Public Sans";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Public_sans/700-normal.ttf), format("truetype");
|
||||
}
|
||||
BIN
src/ui/app/static/fonts/Public_sans/300-italic.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/300-normal.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/400-italic.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/400-normal.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/500-italic.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/500-normal.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/600-italic.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/600-normal.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/700-italic.ttf
Normal file
BIN
src/ui/app/static/fonts/Public_sans/700-normal.ttf
Normal file
1
src/ui/app/static/fonts/boxicons.min.css
vendored
Normal file
BIN
src/ui/app/static/fonts/boxicons/boxicons.eot
Normal file
1653
src/ui/app/static/fonts/boxicons/boxicons.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/ui/app/static/fonts/boxicons/boxicons.ttf
Normal file
BIN
src/ui/app/static/fonts/boxicons/boxicons.woff
Normal file
BIN
src/ui/app/static/fonts/boxicons/boxicons.woff2
Normal file
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
BIN
src/ui/app/static/img/square-blue.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/ui/app/static/img/square-white.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
477
src/ui/app/static/js/buttons.js
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
/*!
|
||||
* github-buttons v2.29.0
|
||||
* (c) 2024 なつき
|
||||
* @license BSD-2-Clause
|
||||
*/
|
||||
!(function () {
|
||||
"use strict";
|
||||
var e = window.document,
|
||||
o = e.location,
|
||||
t = window.Math,
|
||||
r = window.HTMLElement,
|
||||
a = window.XMLHttpRequest,
|
||||
n = "github-button",
|
||||
i = "https://buttons.github.io/buttons.html",
|
||||
c = "github.com",
|
||||
l = "https://api." + c,
|
||||
d = a && "prototype" in a && "withCredentials" in a.prototype,
|
||||
s =
|
||||
d &&
|
||||
r &&
|
||||
"attachShadow" in r.prototype &&
|
||||
!("prototype" in r.prototype.attachShadow),
|
||||
u = function (e, o) {
|
||||
for (var t = 0, r = e.length; t < r; t++) o(e[t]);
|
||||
},
|
||||
f = function (e) {
|
||||
return function (o, t, r) {
|
||||
var a = e.createElement(o);
|
||||
if (null != t)
|
||||
for (var n in t) {
|
||||
var i = t[n];
|
||||
null != i && (null != a[n] ? (a[n] = i) : a.setAttribute(n, i));
|
||||
}
|
||||
return (
|
||||
null != r &&
|
||||
u(r, function (o) {
|
||||
a.appendChild("string" == typeof o ? e.createTextNode(o) : o);
|
||||
}),
|
||||
a
|
||||
);
|
||||
};
|
||||
},
|
||||
h = f(e),
|
||||
g = function (e) {
|
||||
var o;
|
||||
return function () {
|
||||
o || ((o = 1), e.apply(this, arguments));
|
||||
};
|
||||
},
|
||||
p = function (e, o) {
|
||||
return {}.hasOwnProperty.call(e, o);
|
||||
},
|
||||
b = function (e) {
|
||||
return ("" + e).toLowerCase();
|
||||
},
|
||||
v = function (e, o, t, r) {
|
||||
null == o && (o = "&"),
|
||||
null == t && (t = "="),
|
||||
null == r && (r = window.decodeURIComponent);
|
||||
var a = {};
|
||||
return (
|
||||
u(e.split(o), function (e) {
|
||||
if ("" !== e) {
|
||||
var o = e.split(t);
|
||||
a[r(o[0])] = null != o[1] ? r(o.slice(1).join(t)) : void 0;
|
||||
}
|
||||
}),
|
||||
a
|
||||
);
|
||||
},
|
||||
m = function (e, o, t) {
|
||||
e.addEventListener
|
||||
? e.addEventListener(o, t, !1)
|
||||
: e.attachEvent("on" + o, t);
|
||||
},
|
||||
w = function (e, o, t) {
|
||||
e.removeEventListener
|
||||
? e.removeEventListener(o, t, !1)
|
||||
: e.detachEvent("on" + o, t);
|
||||
},
|
||||
k = function (e, o, t) {
|
||||
var r = function () {
|
||||
return w(e, o, r), t.apply(this, arguments);
|
||||
};
|
||||
m(e, o, r);
|
||||
},
|
||||
x = function (e, o, t) {
|
||||
if (null != e.readyState) {
|
||||
var r = "readystatechange",
|
||||
a = function () {
|
||||
if (o.test(e.readyState))
|
||||
return w(e, r, a), t.apply(this, arguments);
|
||||
};
|
||||
m(e, r, a);
|
||||
}
|
||||
},
|
||||
y = {
|
||||
light:
|
||||
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #0969da;outline-offset:-2px}.btn{color:#25292e;background-color:#ebf0f4;border-color:#d1d9e0;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f6f8fa'/%3e%3cstop offset='90%25' stop-color='%23ebf0f4'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);background-image:linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF6F8FA', endColorstr='#FFEAEFF3')}:root .btn{filter:none}.btn:hover,.btn:focus{background-color:#e5eaee;background-position:0 -0.5em;border-color:#d1d9e0;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23eff2f5'/%3e%3cstop offset='90%25' stop-color='%23e5eaee'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #eff2f5, #e5eaee 90%);background-image:linear-gradient(180deg, #eff2f5, #e5eaee 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFEFF2F5', endColorstr='#FFE4E9ED')}:root .btn:hover,:root .btn:focus{filter:none}.btn:active{background-color:#e6eaef;border-color:#d1d9e0;background-image:none;filter:none}.social-count{color:#25292e;background-color:#fff;border-color:#d1d9e0}.social-count:hover,.social-count:focus{color:#0969da}.octicon-heart{color:#bf3989}",
|
||||
light_high_contrast:
|
||||
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #0349b4;outline-offset:-2px}.btn{color:#25292e;background-color:#e0e6eb;border-color:#454c54;background-image:none;filter:none}.btn:hover,.btn:focus{background-color:#d0d7e0;background-position:0 -0.5em;border-color:#454c54;background-image:none;filter:none}.btn:active{background-color:#d1d9e0;border-color:#454c54}.social-count{color:#25292e;background-color:#fff;border-color:#454c54}.social-count:hover,.social-count:focus{color:#023b95}.octicon-heart{color:#7d0c57}",
|
||||
dark: ".btn:focus-visible,.social-count:focus-visible{outline:2px solid #1f6feb;outline-offset:-2px}.btn{color:#f0f6fc;background-color:#1a2026;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23212830'/%3e%3cstop offset='90%25' stop-color='%231a2026'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #212830, #1a2026 90%);background-image:linear-gradient(180deg, #212830, #1a2026 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF212830', endColorstr='#FF191F25')}:root .btn{filter:none}.btn:hover,.btn:focus{background-color:#1f242c;background-position:0 -0.5em;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23262c36'/%3e%3cstop offset='90%25' stop-color='%231f242c'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #262c36, #1f242c 90%);background-image:linear-gradient(180deg, #262c36, #1f242c 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF262C36', endColorstr='#FF1E232B')}:root .btn:hover,:root .btn:focus{filter:none}.btn:active{background-color:#2a313c;border-color:#3d444d;background-image:none;filter:none}.social-count{color:#f0f6fc;background-color:#0d1117;border-color:#3d444d}.social-count:hover,.social-count:focus{color:#388bfd}.octicon-heart{color:#db61a2}",
|
||||
dark_dimmed:
|
||||
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #316dca;outline-offset:-2px}.btn{color:#d1d7e0;background-color:#232932;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%232a313c'/%3e%3cstop offset='90%25' stop-color='%23232932'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #2a313c, #232932 90%);background-image:linear-gradient(180deg, #2a313c, #232932 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF2A313C', endColorstr='#FF222831')}:root .btn{filter:none}.btn:hover,.btn:focus{background-color:#282f38;background-position:0 -0.5em;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%232f3742'/%3e%3cstop offset='90%25' stop-color='%23282f38'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #2f3742, #282f38 90%);background-image:linear-gradient(180deg, #2f3742, #282f38 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF2F3742', endColorstr='#FF272E37')}:root .btn:hover,:root .btn:focus{filter:none}.btn:active{background-color:#3d444d;border-color:#3d444d;background-image:none;filter:none}.social-count{color:#d1d7e0;background-color:#212830;border-color:#3d444d}.social-count:hover,.social-count:focus{color:#4184e4}.octicon-heart{color:#c96198}",
|
||||
dark_high_contrast:
|
||||
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #409eff;outline-offset:-2px}.btn{color:#fff;background-color:#262c36;border-color:#b7bdc8;background-image:none;filter:none}.btn:hover,.btn:focus{background-color:#232932;background-position:0 -0.5em;border-color:#b7bdc8;background-image:none;filter:none}.btn:active{background-color:#2f3742;border-color:#b7bdc8}.social-count{color:#fff;background-color:#010409;border-color:#b7bdc8}.social-count:hover,.social-count:focus{color:#5cacff}.octicon-heart{color:#ff90c8}",
|
||||
},
|
||||
C = function (e, o) {
|
||||
return (
|
||||
"@media(prefers-color-scheme:" + e + "){" + y[p(y, o) ? o : e] + "}"
|
||||
);
|
||||
},
|
||||
M = {
|
||||
"comment-discussion": {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
download: {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path><path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
eye: {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
heart: {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
"issue-opened": {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"></path><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
"mark-github": {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
package: {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="m8.878.392 5.25 3.045c.54.314.872.89.872 1.514v6.098a1.75 1.75 0 0 1-.872 1.514l-5.25 3.045a1.75 1.75 0 0 1-1.756 0l-5.25-3.045A1.75 1.75 0 0 1 1 11.049V4.951c0-.624.332-1.201.872-1.514L7.122.392a1.75 1.75 0 0 1 1.756 0ZM7.875 1.69l-4.63 2.685L8 7.133l4.755-2.758-4.63-2.685a.248.248 0 0 0-.25 0ZM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432Zm6.25 8.271 4.625-2.683a.25.25 0 0 0 .125-.216V5.677L8.75 8.432Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
play: {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
"repo-forked": {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
"repo-template": {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M13.25 8a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-.75a.75.75 0 0 1 0-1.5h.75v-.25a.75.75 0 0 1 .75-.75ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2ZM2.75 8a.75.75 0 0 1 .75.75v.268c.083-.012.166-.018.25-.018h.5a.75.75 0 0 1 0 1.5h-.5a.25.25 0 0 0-.25.25v.75c0 .28.114.532.3.714a.75.75 0 1 1-1.05 1.072A2.495 2.495 0 0 1 2 11.5V8.75A.75.75 0 0 1 2.75 8ZM11 .75a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V1.5h-.75A.75.75 0 0 1 11 .75Zm-5 0A.75.75 0 0 1 6.75 0h2.5a.75.75 0 0 1 0 1.5h-2.5A.75.75 0 0 1 6 .75Zm0 9A.75.75 0 0 1 6.75 9h2.5a.75.75 0 0 1 0 1.5h-2.5A.75.75 0 0 1 6 9.75ZM4.992.662a.75.75 0 0 1-.636.848c-.436.063-.783.41-.846.846a.751.751 0 0 1-1.485-.212A2.501 2.501 0 0 1 4.144.025a.75.75 0 0 1 .848.637ZM2.75 4a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 2.75 4Zm10.5 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
star: {
|
||||
heights: {
|
||||
16: {
|
||||
width: 16,
|
||||
path: '<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Z = function (e, o) {
|
||||
(e = b(e).replace(/^octicon-/, "")), p(M, e) || (e = "mark-github");
|
||||
var t = o >= 24 && 24 in M[e].heights ? 24 : 16,
|
||||
r = M[e].heights[t];
|
||||
return (
|
||||
'<svg viewBox="0 0 ' +
|
||||
r.width +
|
||||
" " +
|
||||
t +
|
||||
'" width="' +
|
||||
(o * r.width) / t +
|
||||
'" height="' +
|
||||
o +
|
||||
'" class="octicon octicon-' +
|
||||
e +
|
||||
'" aria-hidden="true">' +
|
||||
r.path +
|
||||
"</svg>"
|
||||
);
|
||||
},
|
||||
A = {},
|
||||
F = function (e, o) {
|
||||
var t = A[e] || (A[e] = []);
|
||||
if (!(t.push(o) > 1)) {
|
||||
var r = g(function () {
|
||||
for (delete A[e]; (o = t.shift()); ) o.apply(null, arguments);
|
||||
});
|
||||
if (d) {
|
||||
var n = new a();
|
||||
m(n, "abort", r),
|
||||
m(n, "error", r),
|
||||
m(n, "load", function () {
|
||||
var e;
|
||||
try {
|
||||
e = JSON.parse(this.responseText);
|
||||
} catch (e) {
|
||||
return void r(e);
|
||||
}
|
||||
r(200 !== this.status, e);
|
||||
}),
|
||||
n.open("GET", e),
|
||||
n.send();
|
||||
} else {
|
||||
var i = this || window;
|
||||
i._ = function (e) {
|
||||
(i._ = null), r(200 !== e.meta.status, e.data);
|
||||
};
|
||||
var c = f(i.document)("script", {
|
||||
async: !0,
|
||||
src: e + (-1 !== e.indexOf("?") ? "&" : "?") + "callback=_",
|
||||
}),
|
||||
l = function () {
|
||||
i._ && i._({ meta: {} });
|
||||
};
|
||||
m(c, "load", l),
|
||||
m(c, "error", l),
|
||||
x(c, /de|m/, l),
|
||||
i.document.getElementsByTagName("head")[0].appendChild(c);
|
||||
}
|
||||
}
|
||||
},
|
||||
E = function (e, o, t) {
|
||||
var r = f(e.ownerDocument),
|
||||
a = e.appendChild(r("style", { type: "text/css" })),
|
||||
n =
|
||||
"body{margin:0}a{text-decoration:none;outline:0}.widget{display:inline-block;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;font-size:0;line-height:0;white-space:nowrap}.btn,.social-count{position:relative;display:inline-block;display:inline-flex;height:14px;padding:2px 5px;font-size:11px;font-weight:600;line-height:14px;vertical-align:bottom;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-repeat:repeat-x;background-position:-1px -1px;background-size:110% 110%;border:1px solid}.btn{border-radius:.25em}.btn:not(:last-child){border-radius:.25em 0 0 .25em}.social-count{border-left:0;border-radius:0 .25em .25em 0}.widget-lg .btn,.widget-lg .social-count{height:16px;padding:5px 10px;font-size:12px;line-height:16px}.octicon{display:inline-block;vertical-align:text-top;fill:currentColor;overflow:visible}" +
|
||||
(function (e) {
|
||||
if (null == e) return y.light;
|
||||
if (p(y, e)) return y[e];
|
||||
var o = v(e, ";", ":", function (e) {
|
||||
return e.replace(/^[ \t\n\f\r]+|[ \t\n\f\r]+$/g, "");
|
||||
});
|
||||
return (
|
||||
y[p(y, o["no-preference"]) ? o["no-preference"] : "light"] +
|
||||
C("light", o.light) +
|
||||
C("dark", o.dark)
|
||||
);
|
||||
})(o["data-color-scheme"]);
|
||||
a.styleSheet
|
||||
? (a.styleSheet.cssText = n)
|
||||
: a.appendChild(e.ownerDocument.createTextNode(n));
|
||||
var i = "large" === b(o["data-size"]),
|
||||
d = r(
|
||||
"a",
|
||||
{
|
||||
className: "btn",
|
||||
href: o.href,
|
||||
rel: "noopener",
|
||||
target: "_blank",
|
||||
title: o.title || void 0,
|
||||
"aria-label": o["aria-label"] || void 0,
|
||||
innerHTML: Z(o["data-icon"], i ? 16 : 14) + " ",
|
||||
},
|
||||
[r("span", {}, [o["data-text"] || ""])],
|
||||
),
|
||||
s = e.appendChild(
|
||||
r("div", { className: "widget" + (i ? " widget-lg" : "") }, [d]),
|
||||
),
|
||||
u = d.hostname.replace(/\.$/, "");
|
||||
if (("." + u).substring(u.length - 10) !== "." + c)
|
||||
return d.removeAttribute("href"), void t(s);
|
||||
var h = (" /" + d.pathname).split(/\/+/);
|
||||
if (
|
||||
((((u === c || u === "gist." + c) && "archive" === h[3]) ||
|
||||
(u === c &&
|
||||
"releases" === h[3] &&
|
||||
("download" === h[4] ||
|
||||
("latest" === h[4] && "download" === h[5]))) ||
|
||||
u === "codeload." + c) &&
|
||||
(d.target = "_top"),
|
||||
"true" === b(o["data-show-count"]) &&
|
||||
u === c &&
|
||||
"marketplace" !== h[1] &&
|
||||
"sponsors" !== h[1] &&
|
||||
"orgs" !== h[1] &&
|
||||
"users" !== h[1] &&
|
||||
"-" !== h[1])
|
||||
) {
|
||||
var g, m;
|
||||
if (!h[2] && h[1]) (m = "followers"), (g = "?tab=followers");
|
||||
else if (!h[3] && h[2]) (m = "stargazers_count"), (g = "/stargazers");
|
||||
else if (h[4] || "subscription" !== h[3])
|
||||
if (h[4] || "fork" !== h[3]) {
|
||||
if ("issues" !== h[3]) return void t(s);
|
||||
(m = "open_issues_count"), (g = "/issues");
|
||||
} else (m = "forks_count"), (g = "/forks");
|
||||
else (m = "subscribers_count"), (g = "/watchers");
|
||||
var w = h[2] ? "/repos/" + h[1] + "/" + h[2] : "/users/" + h[1];
|
||||
F.call(this, l + w, function (e, o) {
|
||||
if (!e) {
|
||||
var a = o[m];
|
||||
s.appendChild(
|
||||
r(
|
||||
"a",
|
||||
{
|
||||
className: "social-count",
|
||||
href: o.html_url + g,
|
||||
rel: "noopener",
|
||||
target: "_blank",
|
||||
"aria-label":
|
||||
a +
|
||||
" " +
|
||||
m
|
||||
.replace(/_count$/, "")
|
||||
.replace("_", " ")
|
||||
.slice(0, a < 2 ? -1 : void 0) +
|
||||
" on GitHub",
|
||||
},
|
||||
[("" + a).replace(/\B(?=(\d{3})+(?!\d))/g, ",")],
|
||||
),
|
||||
);
|
||||
}
|
||||
t(s);
|
||||
});
|
||||
} else t(s);
|
||||
},
|
||||
L = window.devicePixelRatio || 1,
|
||||
_ = function (e) {
|
||||
return (L > 1 ? t.ceil((t.round(e * L) / L) * 2) / 2 : t.ceil(e)) || 0;
|
||||
},
|
||||
G = function (e, o) {
|
||||
(e.style.width = o[0] + "px"), (e.style.height = o[1] + "px");
|
||||
},
|
||||
T = function (o, r) {
|
||||
if (null != o && null != r)
|
||||
if (
|
||||
(o.getAttribute &&
|
||||
(o = (function (e) {
|
||||
var o = {
|
||||
href: e.href,
|
||||
title: e.title,
|
||||
"aria-label": e.getAttribute("aria-label"),
|
||||
};
|
||||
return (
|
||||
u(
|
||||
["icon", "color-scheme", "text", "size", "show-count"],
|
||||
function (t) {
|
||||
var r = "data-" + t;
|
||||
o[r] = e.getAttribute(r);
|
||||
},
|
||||
),
|
||||
null == o["data-text"] &&
|
||||
(o["data-text"] = e.textContent || e.innerText),
|
||||
o
|
||||
);
|
||||
})(o)),
|
||||
s)
|
||||
) {
|
||||
var a = h("span");
|
||||
E(a.attachShadow({ mode: "closed" }), o, function () {
|
||||
r(a);
|
||||
});
|
||||
} else {
|
||||
var n = h("iframe", {
|
||||
src: "javascript:0",
|
||||
title: o.title || void 0,
|
||||
allowtransparency: !0,
|
||||
scrolling: "no",
|
||||
frameBorder: 0,
|
||||
});
|
||||
G(n, [0, 0]), (n.style.border = "none");
|
||||
var c = function () {
|
||||
var a,
|
||||
l = n.contentWindow;
|
||||
try {
|
||||
a = l.document.body;
|
||||
} catch (o) {
|
||||
return void e.body.appendChild(n.parentNode.removeChild(n));
|
||||
}
|
||||
w(n, "load", c),
|
||||
E.call(l, a, o, function (e) {
|
||||
var a = (function (e) {
|
||||
var o = e.offsetWidth,
|
||||
r = e.offsetHeight;
|
||||
if (e.getBoundingClientRect) {
|
||||
var a = e.getBoundingClientRect();
|
||||
(o = t.max(o, _(a.width))), (r = t.max(r, _(a.height)));
|
||||
}
|
||||
return [o, r];
|
||||
})(e);
|
||||
n.parentNode.removeChild(n),
|
||||
k(n, "load", function () {
|
||||
G(n, a);
|
||||
}),
|
||||
(n.src =
|
||||
i +
|
||||
"#" +
|
||||
(n.name = (function (e, o, t, r) {
|
||||
null == o && (o = "&"),
|
||||
null == t && (t = "="),
|
||||
null == r && (r = window.encodeURIComponent);
|
||||
var a = [];
|
||||
for (var n in e) {
|
||||
var i = e[n];
|
||||
null != i && a.push(r(n) + t + r(i));
|
||||
}
|
||||
return a.join(o);
|
||||
})(o))),
|
||||
r(n);
|
||||
});
|
||||
};
|
||||
m(n, "load", c), e.body.appendChild(n);
|
||||
}
|
||||
};
|
||||
o.protocol + "//" + o.host + o.pathname === i
|
||||
? E(e.body, v(window.name || o.hash.replace(/^#/, "")), function () {})
|
||||
: (function (o) {
|
||||
if (
|
||||
"complete" === e.readyState ||
|
||||
("loading" !== e.readyState && !e.documentElement.doScroll)
|
||||
)
|
||||
setTimeout(o);
|
||||
else if (e.addEventListener) {
|
||||
var t = g(o);
|
||||
k(e, "DOMContentLoaded", t), k(window, "load", t);
|
||||
} else x(e, /m/, o);
|
||||
})(function () {
|
||||
var o,
|
||||
t = e.querySelectorAll
|
||||
? e.querySelectorAll("a." + n)
|
||||
: ((o = []),
|
||||
u(e.getElementsByTagName("a"), function (e) {
|
||||
-1 !==
|
||||
(" " + e.className + " ")
|
||||
.replace(/[ \t\n\f\r]+/g, " ")
|
||||
.indexOf(" " + n + " ") && o.push(e);
|
||||
}),
|
||||
o);
|
||||
u(t, function (e) {
|
||||
T(e, function (o) {
|
||||
e.parentNode.replaceChild(o, e);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
29
src/ui/app/static/js/config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Config
|
||||
* -------------------------------------------------------------------------------------
|
||||
* ! IMPORTANT: Make sure you clear the browser local storage In order to see the config changes in the template.
|
||||
* ! To clear local storage: (https://www.leadshook.com/help/how-to-clear-local-storage-in-google-chrome-browser/).
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// JS global variables
|
||||
window.config = {
|
||||
colors: {
|
||||
primary: "#0b5577",
|
||||
secondary: "#2eac68",
|
||||
success: "#71dd37",
|
||||
info: "#03c3ec",
|
||||
warning: "#ffab00",
|
||||
danger: "#ff3e1d",
|
||||
dark: "#233446",
|
||||
black: "#22303e",
|
||||
white: "#fff",
|
||||
cardColor: "#fff",
|
||||
bodyBg: "#f5f5f9",
|
||||
bodyColor: "#646E78",
|
||||
headingColor: "#384551",
|
||||
textMuted: "#a7acb2",
|
||||
borderColor: "#e4e6e8",
|
||||
},
|
||||
};
|
||||
848
src/ui/app/static/js/dashboards-analytics.js
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
/**
|
||||
* Dashboard Analytics
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
let cardColor, headingColor, legendColor, labelColor, shadeColor, borderColor;
|
||||
|
||||
cardColor = config.colors.cardColor;
|
||||
headingColor = config.colors.headingColor;
|
||||
legendColor = config.colors.bodyColor;
|
||||
labelColor = config.colors.textMuted;
|
||||
borderColor = config.colors.borderColor;
|
||||
|
||||
// Order Area Chart
|
||||
// --------------------------------------------------------------------
|
||||
const orderAreaChartEl = document.querySelector("#orderChart"),
|
||||
orderAreaChartConfig = {
|
||||
chart: {
|
||||
height: 80,
|
||||
type: "area",
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 6,
|
||||
colors: "transparent",
|
||||
strokeColors: "transparent",
|
||||
strokeWidth: 4,
|
||||
discrete: [
|
||||
{
|
||||
fillColor: cardColor,
|
||||
seriesIndex: 0,
|
||||
dataPointIndex: 6,
|
||||
strokeColor: config.colors.success,
|
||||
strokeWidth: 2,
|
||||
size: 6,
|
||||
radius: 8,
|
||||
},
|
||||
],
|
||||
hover: {
|
||||
size: 7,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
right: 8,
|
||||
},
|
||||
},
|
||||
colors: [config.colors.success],
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shade: shadeColor,
|
||||
shadeIntensity: 0.8,
|
||||
opacityFrom: 0.8,
|
||||
opacityTo: 0.25,
|
||||
stops: [0, 85, 100],
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
width: 2,
|
||||
curve: "smooth",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [180, 175, 275, 140, 205, 190, 295],
|
||||
},
|
||||
],
|
||||
xaxis: {
|
||||
show: false,
|
||||
lines: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
stroke: {
|
||||
width: 0,
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
stroke: {
|
||||
width: 0,
|
||||
},
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
if (typeof orderAreaChartEl !== undefined && orderAreaChartEl !== null) {
|
||||
const orderAreaChart = new ApexCharts(
|
||||
orderAreaChartEl,
|
||||
orderAreaChartConfig,
|
||||
);
|
||||
orderAreaChart.render();
|
||||
}
|
||||
|
||||
// Total Revenue Report Chart - Bar Chart
|
||||
// --------------------------------------------------------------------
|
||||
const totalRevenueChartEl = document.querySelector("#totalRevenueChart"),
|
||||
totalRevenueChartOptions = {
|
||||
series: [
|
||||
{
|
||||
name: new Date().getFullYear() - 1,
|
||||
|
||||
data: [18, 7, 15, 29, 18, 12, 9],
|
||||
},
|
||||
{
|
||||
name: new Date().getFullYear() - 2,
|
||||
data: [-13, -18, -9, -14, -5, -17, -15],
|
||||
},
|
||||
],
|
||||
chart: {
|
||||
height: 317,
|
||||
stacked: true,
|
||||
type: "bar",
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: "30%",
|
||||
borderRadius: 8,
|
||||
startingShape: "rounded",
|
||||
endingShape: "rounded",
|
||||
},
|
||||
},
|
||||
colors: [config.colors.primary, config.colors.info],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 6,
|
||||
lineCap: "round",
|
||||
colors: [cardColor],
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
horizontalAlign: "left",
|
||||
position: "top",
|
||||
markers: {
|
||||
height: 8,
|
||||
width: 8,
|
||||
radius: 12,
|
||||
offsetX: -5,
|
||||
},
|
||||
fontSize: "13px",
|
||||
fontFamily: "Public Sans",
|
||||
fontWeight: 400,
|
||||
labels: {
|
||||
colors: legendColor,
|
||||
useSeriesColors: false,
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 7,
|
||||
borderColor: borderColor,
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: -8,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: [1, 1],
|
||||
},
|
||||
xaxis: {
|
||||
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: "13px",
|
||||
fontFamily: "Public Sans",
|
||||
colors: labelColor,
|
||||
},
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: "13px",
|
||||
fontFamily: "Public Sans",
|
||||
colors: labelColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1700,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: "35%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1440,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 12,
|
||||
columnWidth: "43%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1300,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 11,
|
||||
columnWidth: "45%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1200,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 11,
|
||||
columnWidth: "37%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1040,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 12,
|
||||
columnWidth: "45%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 991,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 12,
|
||||
columnWidth: "33%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 11,
|
||||
columnWidth: "28%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 640,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 11,
|
||||
columnWidth: "30%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 576,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: "38%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 440,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: "50%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 380,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 9,
|
||||
columnWidth: "60%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
active: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (
|
||||
typeof totalRevenueChartEl !== undefined &&
|
||||
totalRevenueChartEl !== null
|
||||
) {
|
||||
const totalRevenueChart = new ApexCharts(
|
||||
totalRevenueChartEl,
|
||||
totalRevenueChartOptions,
|
||||
);
|
||||
totalRevenueChart.render();
|
||||
}
|
||||
|
||||
// Growth Chart - Radial Bar Chart
|
||||
// --------------------------------------------------------------------
|
||||
const growthChartEl = document.querySelector("#growthChart"),
|
||||
growthChartOptions = {
|
||||
series: [78],
|
||||
labels: ["Growth"],
|
||||
chart: {
|
||||
height: 240,
|
||||
type: "radialBar",
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
size: 150,
|
||||
offsetY: 10,
|
||||
startAngle: -150,
|
||||
endAngle: 150,
|
||||
hollow: {
|
||||
size: "55%",
|
||||
},
|
||||
track: {
|
||||
background: cardColor,
|
||||
strokeWidth: "100%",
|
||||
},
|
||||
dataLabels: {
|
||||
name: {
|
||||
offsetY: 15,
|
||||
color: legendColor,
|
||||
fontSize: "15px",
|
||||
fontWeight: "500",
|
||||
fontFamily: "Public Sans",
|
||||
},
|
||||
value: {
|
||||
offsetY: -25,
|
||||
color: headingColor,
|
||||
fontSize: "22px",
|
||||
fontWeight: "500",
|
||||
fontFamily: "Public Sans",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
colors: [config.colors.primary],
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shade: "dark",
|
||||
shadeIntensity: 0.5,
|
||||
gradientToColors: [config.colors.primary],
|
||||
inverseColors: true,
|
||||
opacityFrom: 1,
|
||||
opacityTo: 0.6,
|
||||
stops: [30, 70, 100],
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
dashArray: 5,
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
top: -35,
|
||||
bottom: -10,
|
||||
},
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
active: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (typeof growthChartEl !== undefined && growthChartEl !== null) {
|
||||
const growthChart = new ApexCharts(growthChartEl, growthChartOptions);
|
||||
growthChart.render();
|
||||
}
|
||||
|
||||
// Revenue Bar Chart
|
||||
// --------------------------------------------------------------------
|
||||
const revenueBarChartEl = document.querySelector("#revenueChart"),
|
||||
revenueBarChartConfig = {
|
||||
chart: {
|
||||
height: 95,
|
||||
type: "bar",
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
barHeight: "80%",
|
||||
columnWidth: "75%",
|
||||
startingShape: "rounded",
|
||||
endingShape: "rounded",
|
||||
borderRadius: 4,
|
||||
distributed: true,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
top: -20,
|
||||
bottom: -12,
|
||||
left: -10,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
colors: [
|
||||
config.colors.primary,
|
||||
config.colors.primary,
|
||||
config.colors.primary,
|
||||
config.colors.primary,
|
||||
config.colors.primary,
|
||||
config.colors.primary,
|
||||
config.colors.primary,
|
||||
],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [40, 95, 60, 45, 90, 50, 75],
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
xaxis: {
|
||||
categories: ["M", "T", "W", "T", "F", "S", "S"],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontSize: "13px",
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (typeof revenueBarChartEl !== undefined && revenueBarChartEl !== null) {
|
||||
const revenueBarChart = new ApexCharts(
|
||||
revenueBarChartEl,
|
||||
revenueBarChartConfig,
|
||||
);
|
||||
revenueBarChart.render();
|
||||
}
|
||||
|
||||
// Profit Report Line Chart
|
||||
// --------------------------------------------------------------------
|
||||
const profileReportChartEl = document.querySelector("#profileReportChart"),
|
||||
profileReportChartConfig = {
|
||||
chart: {
|
||||
height: 75,
|
||||
// width: 175,
|
||||
type: "line",
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
top: 10,
|
||||
left: 5,
|
||||
blur: 3,
|
||||
color: config.colors.warning,
|
||||
opacity: 0.15,
|
||||
},
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
right: 8,
|
||||
},
|
||||
},
|
||||
colors: [config.colors.warning],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
width: 5,
|
||||
curve: "smooth",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [110, 270, 145, 245, 205, 285],
|
||||
},
|
||||
],
|
||||
xaxis: {
|
||||
show: false,
|
||||
lines: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
if (
|
||||
typeof profileReportChartEl !== undefined &&
|
||||
profileReportChartEl !== null
|
||||
) {
|
||||
const profileReportChart = new ApexCharts(
|
||||
profileReportChartEl,
|
||||
profileReportChartConfig,
|
||||
);
|
||||
profileReportChart.render();
|
||||
}
|
||||
|
||||
// Order Statistics Chart
|
||||
// --------------------------------------------------------------------
|
||||
const chartOrderStatistics = document.querySelector("#orderStatisticsChart"),
|
||||
orderChartConfig = {
|
||||
chart: {
|
||||
height: 145,
|
||||
width: 110,
|
||||
type: "donut",
|
||||
},
|
||||
labels: ["Electronic", "Sports", "Decor", "Fashion"],
|
||||
series: [50, 85, 25, 40],
|
||||
colors: [
|
||||
config.colors.success,
|
||||
config.colors.primary,
|
||||
config.colors.secondary,
|
||||
config.colors.info,
|
||||
],
|
||||
stroke: {
|
||||
width: 5,
|
||||
colors: [cardColor],
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
formatter: function (val, opt) {
|
||||
return parseInt(val) + "%";
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 15,
|
||||
},
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: "none" },
|
||||
},
|
||||
active: {
|
||||
filter: { type: "none" },
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: "75%",
|
||||
labels: {
|
||||
show: true,
|
||||
value: {
|
||||
fontSize: "18px",
|
||||
fontFamily: "Public Sans",
|
||||
fontWeight: 500,
|
||||
color: headingColor,
|
||||
offsetY: -17,
|
||||
formatter: function (val) {
|
||||
return parseInt(val) + "%";
|
||||
},
|
||||
},
|
||||
name: {
|
||||
offsetY: 17,
|
||||
fontFamily: "Public Sans",
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
fontSize: "13px",
|
||||
color: legendColor,
|
||||
label: "Weekly",
|
||||
formatter: function (w) {
|
||||
return "38%";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (
|
||||
typeof chartOrderStatistics !== undefined &&
|
||||
chartOrderStatistics !== null
|
||||
) {
|
||||
const statisticsChart = new ApexCharts(
|
||||
chartOrderStatistics,
|
||||
orderChartConfig,
|
||||
);
|
||||
statisticsChart.render();
|
||||
}
|
||||
|
||||
// Income Chart - Area chart
|
||||
// --------------------------------------------------------------------
|
||||
const incomeChartEl = document.querySelector("#incomeChart"),
|
||||
incomeChartConfig = {
|
||||
series: [
|
||||
{
|
||||
data: [21, 30, 22, 42, 26, 35, 29],
|
||||
},
|
||||
],
|
||||
chart: {
|
||||
height: 232,
|
||||
parentHeightOffset: 0,
|
||||
parentWidthOffset: 0,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
type: "area",
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
curve: "smooth",
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
markers: {
|
||||
size: 6,
|
||||
colors: "transparent",
|
||||
strokeColors: "transparent",
|
||||
strokeWidth: 4,
|
||||
discrete: [
|
||||
{
|
||||
fillColor: config.colors.white,
|
||||
seriesIndex: 0,
|
||||
dataPointIndex: 6,
|
||||
strokeColor: config.colors.primary,
|
||||
strokeWidth: 2,
|
||||
size: 6,
|
||||
radius: 8,
|
||||
},
|
||||
],
|
||||
hover: {
|
||||
size: 7,
|
||||
},
|
||||
},
|
||||
colors: [config.colors.primary],
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shade: shadeColor,
|
||||
shadeIntensity: 0.6,
|
||||
opacityFrom: 0.5,
|
||||
opacityTo: 0.25,
|
||||
stops: [0, 95, 100],
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: borderColor,
|
||||
strokeDashArray: 8,
|
||||
padding: {
|
||||
top: -20,
|
||||
bottom: -8,
|
||||
left: 0,
|
||||
right: 8,
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
fontSize: "13px",
|
||||
colors: labelColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
min: 10,
|
||||
max: 50,
|
||||
tickAmount: 4,
|
||||
},
|
||||
};
|
||||
if (typeof incomeChartEl !== undefined && incomeChartEl !== null) {
|
||||
const incomeChart = new ApexCharts(incomeChartEl, incomeChartConfig);
|
||||
incomeChart.render();
|
||||
}
|
||||
|
||||
// Expenses Mini Chart - Radial Chart
|
||||
// --------------------------------------------------------------------
|
||||
const weeklyExpensesEl = document.querySelector("#expensesOfWeek"),
|
||||
weeklyExpensesConfig = {
|
||||
series: [65],
|
||||
chart: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
type: "radialBar",
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
startAngle: 0,
|
||||
endAngle: 360,
|
||||
strokeWidth: "8",
|
||||
hollow: {
|
||||
margin: 2,
|
||||
size: "40%",
|
||||
},
|
||||
track: {
|
||||
background: borderColor,
|
||||
},
|
||||
dataLabels: {
|
||||
show: true,
|
||||
name: {
|
||||
show: false,
|
||||
},
|
||||
value: {
|
||||
formatter: function (val) {
|
||||
return "$" + parseInt(val);
|
||||
},
|
||||
offsetY: 5,
|
||||
color: legendColor,
|
||||
fontSize: "12px",
|
||||
fontFamily: "Public Sans",
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
type: "solid",
|
||||
colors: config.colors.primary,
|
||||
},
|
||||
stroke: {
|
||||
lineCap: "round",
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
top: -10,
|
||||
bottom: -15,
|
||||
left: -10,
|
||||
right: -10,
|
||||
},
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
active: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (typeof weeklyExpensesEl !== undefined && weeklyExpensesEl !== null) {
|
||||
const weeklyExpenses = new ApexCharts(
|
||||
weeklyExpensesEl,
|
||||
weeklyExpensesConfig,
|
||||
);
|
||||
weeklyExpenses.render();
|
||||
}
|
||||
})();
|
||||
37
src/ui/app/static/js/extended-ui-perfect-scrollbar.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Perfect Scrollbar
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
(function () {
|
||||
const verticalExample = document.getElementById("vertical-example"),
|
||||
horizontalExample = document.getElementById("horizontal-example"),
|
||||
horizVertExample = document.getElementById("both-scrollbars-example");
|
||||
|
||||
// Vertical Example
|
||||
// --------------------------------------------------------------------
|
||||
if (verticalExample) {
|
||||
new PerfectScrollbar(verticalExample, {
|
||||
wheelPropagation: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Horizontal Example
|
||||
// --------------------------------------------------------------------
|
||||
if (horizontalExample) {
|
||||
new PerfectScrollbar(horizontalExample, {
|
||||
wheelPropagation: false,
|
||||
suppressScrollY: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Both vertical and Horizontal Example
|
||||
// --------------------------------------------------------------------
|
||||
if (horizVertExample) {
|
||||
new PerfectScrollbar(horizVertExample, {
|
||||
wheelPropagation: false,
|
||||
});
|
||||
}
|
||||
})();
|
||||
});
|
||||
11
src/ui/app/static/js/form-basic-inputs.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Form Basic Inputs
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
// Indeterminate checkbox
|
||||
const checkbox = document.getElementById("defaultCheck2");
|
||||
checkbox.indeterminate = true;
|
||||
})();
|
||||
932
src/ui/app/static/js/helpers.js
Normal file
|
|
@ -0,0 +1,932 @@
|
|||
// Constants
|
||||
const TRANS_EVENTS = ["transitionend", "webkitTransitionEnd", "oTransitionEnd"];
|
||||
const TRANS_PROPERTIES = [
|
||||
"transition",
|
||||
"MozTransition",
|
||||
"webkitTransition",
|
||||
"WebkitTransition",
|
||||
"OTransition",
|
||||
];
|
||||
const INLINE_STYLES = `
|
||||
.layout-menu-fixed .layout-navbar-full .layout-menu,
|
||||
.layout-page {
|
||||
padding-top: {navbarHeight}px !important;
|
||||
}
|
||||
.content-wrapper {
|
||||
padding-bottom: {footerHeight}px !important;
|
||||
}`;
|
||||
|
||||
// Guard
|
||||
function requiredParam(name) {
|
||||
throw new Error(`Parameter required${name ? `: \`${name}\`` : ""}`);
|
||||
}
|
||||
|
||||
const Helpers = {
|
||||
// Root Element
|
||||
ROOT_EL: typeof window !== "undefined" ? document.documentElement : null,
|
||||
|
||||
// Large screens breakpoint
|
||||
LAYOUT_BREAKPOINT: 1200,
|
||||
|
||||
// Resize delay in milliseconds
|
||||
RESIZE_DELAY: 200,
|
||||
|
||||
menuPsScroll: null,
|
||||
|
||||
mainMenu: null,
|
||||
|
||||
// Internal variables
|
||||
_curStyle: null,
|
||||
_styleEl: null,
|
||||
_resizeTimeout: null,
|
||||
_resizeCallback: null,
|
||||
_transitionCallback: null,
|
||||
_transitionCallbackTimeout: null,
|
||||
_listeners: [],
|
||||
_initialized: false,
|
||||
_autoUpdate: false,
|
||||
_lastWindowHeight: 0,
|
||||
|
||||
// *******************************************************************************
|
||||
// * Utilities
|
||||
|
||||
// ---
|
||||
// Scroll To Active Menu Item
|
||||
_scrollToActive(animate = false, duration = 500) {
|
||||
const layoutMenu = this.getLayoutMenu();
|
||||
|
||||
if (!layoutMenu) return;
|
||||
|
||||
let activeEl = layoutMenu.querySelector("li.menu-item.active:not(.open)");
|
||||
|
||||
if (activeEl) {
|
||||
// t = current time
|
||||
// b = start value
|
||||
// c = change in value
|
||||
// d = duration
|
||||
const easeInOutQuad = (t, b, c, d) => {
|
||||
t /= d / 2;
|
||||
if (t < 1) return (c / 2) * t * t + b;
|
||||
t -= 1;
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||
};
|
||||
|
||||
const element = this.getLayoutMenu().querySelector(".menu-inner");
|
||||
|
||||
if (typeof activeEl === "string") {
|
||||
activeEl = document.querySelector(activeEl);
|
||||
}
|
||||
if (typeof activeEl !== "number") {
|
||||
activeEl = activeEl.getBoundingClientRect().top + element.scrollTop;
|
||||
}
|
||||
|
||||
// If active element's top position is less than 2/3 (66%) of menu height than do not scroll
|
||||
if (activeEl < parseInt((element.clientHeight * 2) / 3, 10)) return;
|
||||
|
||||
const start = element.scrollTop;
|
||||
const change = activeEl - start - parseInt(element.clientHeight / 2, 10);
|
||||
const startDate = +new Date();
|
||||
|
||||
if (animate === true) {
|
||||
const animateScroll = () => {
|
||||
const currentDate = +new Date();
|
||||
const currentTime = currentDate - startDate;
|
||||
const val = easeInOutQuad(currentTime, start, change, duration);
|
||||
element.scrollTop = val;
|
||||
if (currentTime < duration) {
|
||||
requestAnimationFrame(animateScroll);
|
||||
} else {
|
||||
element.scrollTop = change;
|
||||
}
|
||||
};
|
||||
animateScroll();
|
||||
} else {
|
||||
element.scrollTop = change;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Add classes
|
||||
_addClass(cls, el = this.ROOT_EL) {
|
||||
if (el && el.length !== undefined) {
|
||||
// Add classes to multiple elements
|
||||
el.forEach((e) => {
|
||||
if (e) {
|
||||
cls.split(" ").forEach((c) => e.classList.add(c));
|
||||
}
|
||||
});
|
||||
} else if (el) {
|
||||
// Add classes to single element
|
||||
cls.split(" ").forEach((c) => el.classList.add(c));
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Remove classes
|
||||
_removeClass(cls, el = this.ROOT_EL) {
|
||||
if (el && el.length !== undefined) {
|
||||
// Remove classes to multiple elements
|
||||
el.forEach((e) => {
|
||||
if (e) {
|
||||
cls.split(" ").forEach((c) => e.classList.remove(c));
|
||||
}
|
||||
});
|
||||
} else if (el) {
|
||||
// Remove classes to single element
|
||||
cls.split(" ").forEach((c) => el.classList.remove(c));
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle classes
|
||||
_toggleClass(el = this.ROOT_EL, cls1, cls2) {
|
||||
if (el.classList.contains(cls1)) {
|
||||
el.classList.replace(cls1, cls2);
|
||||
} else {
|
||||
el.classList.replace(cls2, cls1);
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Has class
|
||||
_hasClass(cls, el = this.ROOT_EL) {
|
||||
let result = false;
|
||||
|
||||
cls.split(" ").forEach((c) => {
|
||||
if (el.classList.contains(c)) result = true;
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
_findParent(el, cls) {
|
||||
if (
|
||||
(el && el.tagName.toUpperCase() === "BODY") ||
|
||||
el.tagName.toUpperCase() === "HTML"
|
||||
)
|
||||
return null;
|
||||
el = el.parentNode;
|
||||
while (
|
||||
el &&
|
||||
el.tagName.toUpperCase() !== "BODY" &&
|
||||
!el.classList.contains(cls)
|
||||
) {
|
||||
el = el.parentNode;
|
||||
}
|
||||
el = el && el.tagName.toUpperCase() !== "BODY" ? el : null;
|
||||
return el;
|
||||
},
|
||||
|
||||
// ---
|
||||
// Trigger window event
|
||||
_triggerWindowEvent(name) {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
if (document.createEvent) {
|
||||
let event;
|
||||
|
||||
if (typeof Event === "function") {
|
||||
event = new Event(name);
|
||||
} else {
|
||||
event = document.createEvent("Event");
|
||||
event.initEvent(name, false, true);
|
||||
}
|
||||
|
||||
window.dispatchEvent(event);
|
||||
} else {
|
||||
window.fireEvent(`on${name}`, document.createEventObject());
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Trigger event
|
||||
_triggerEvent(name) {
|
||||
this._triggerWindowEvent(`layout${name}`);
|
||||
|
||||
this._listeners
|
||||
.filter((listener) => listener.event === name)
|
||||
.forEach((listener) => listener.callback.call(null));
|
||||
},
|
||||
|
||||
// ---
|
||||
// Update style
|
||||
_updateInlineStyle(navbarHeight = 0, footerHeight = 0) {
|
||||
if (!this._styleEl) {
|
||||
this._styleEl = document.createElement("style");
|
||||
this._styleEl.type = "text/css";
|
||||
document.head.appendChild(this._styleEl);
|
||||
}
|
||||
|
||||
const newStyle = INLINE_STYLES.replace(
|
||||
/\{navbarHeight\}/gi,
|
||||
navbarHeight,
|
||||
).replace(/\{footerHeight\}/gi, footerHeight);
|
||||
|
||||
if (this._curStyle !== newStyle) {
|
||||
this._curStyle = newStyle;
|
||||
this._styleEl.textContent = newStyle;
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Remove style
|
||||
_removeInlineStyle() {
|
||||
if (this._styleEl) document.head.removeChild(this._styleEl);
|
||||
this._styleEl = null;
|
||||
this._curStyle = null;
|
||||
},
|
||||
|
||||
// ---
|
||||
// Redraw layout menu (Safari bugfix)
|
||||
_redrawLayoutMenu() {
|
||||
const layoutMenu = this.getLayoutMenu();
|
||||
|
||||
if (layoutMenu && layoutMenu.querySelector(".menu")) {
|
||||
const inner = layoutMenu.querySelector(".menu-inner");
|
||||
const { scrollTop } = inner;
|
||||
const pageScrollTop = document.documentElement.scrollTop;
|
||||
|
||||
layoutMenu.style.display = "none";
|
||||
// layoutMenu.offsetHeight
|
||||
layoutMenu.style.display = "";
|
||||
inner.scrollTop = scrollTop;
|
||||
document.documentElement.scrollTop = pageScrollTop;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// ---
|
||||
// Check for transition support
|
||||
_supportsTransitionEnd() {
|
||||
if (window.QUnit) return false;
|
||||
|
||||
const el = document.body || document.documentElement;
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
let result = false;
|
||||
TRANS_PROPERTIES.forEach((evnt) => {
|
||||
if (typeof el.style[evnt] !== "undefined") result = true;
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// ---
|
||||
// Calculate current navbar height
|
||||
_getNavbarHeight() {
|
||||
const layoutNavbar = this.getLayoutNavbar();
|
||||
|
||||
if (!layoutNavbar) return 0;
|
||||
if (!this.isSmallScreen())
|
||||
return layoutNavbar.getBoundingClientRect().height;
|
||||
|
||||
// Needs some logic to get navbar height on small screens
|
||||
|
||||
const clonedEl = layoutNavbar.cloneNode(true);
|
||||
clonedEl.id = null;
|
||||
clonedEl.style.visibility = "hidden";
|
||||
clonedEl.style.position = "absolute";
|
||||
|
||||
Array.prototype.slice
|
||||
.call(clonedEl.querySelectorAll(".collapse.show"))
|
||||
.forEach((el) => this._removeClass("show", el));
|
||||
|
||||
layoutNavbar.parentNode.insertBefore(clonedEl, layoutNavbar);
|
||||
|
||||
const navbarHeight = clonedEl.getBoundingClientRect().height;
|
||||
|
||||
clonedEl.parentNode.removeChild(clonedEl);
|
||||
|
||||
return navbarHeight;
|
||||
},
|
||||
|
||||
// ---
|
||||
// Get current footer height
|
||||
_getFooterHeight() {
|
||||
const layoutFooter = this.getLayoutFooter();
|
||||
|
||||
if (!layoutFooter) return 0;
|
||||
|
||||
return layoutFooter.getBoundingClientRect().height;
|
||||
},
|
||||
|
||||
// ---
|
||||
// Get animation duration of element
|
||||
_getAnimationDuration(el) {
|
||||
const duration = window.getComputedStyle(el).transitionDuration;
|
||||
|
||||
return parseFloat(duration) * (duration.indexOf("ms") !== -1 ? 1 : 1000);
|
||||
},
|
||||
|
||||
// ---
|
||||
// Set menu hover state
|
||||
_setMenuHoverState(hovered) {
|
||||
this[hovered ? "_addClass" : "_removeClass"]("layout-menu-hover");
|
||||
},
|
||||
|
||||
// ---
|
||||
// Toggle collapsed
|
||||
_setCollapsed(collapsed) {
|
||||
if (this.isSmallScreen()) {
|
||||
if (collapsed) {
|
||||
this._removeClass("layout-menu-expanded");
|
||||
} else {
|
||||
setTimeout(
|
||||
() => {
|
||||
this._addClass("layout-menu-expanded");
|
||||
},
|
||||
this._redrawLayoutMenu() ? 5 : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Add layout sivenav toggle animationEnd event
|
||||
_bindLayoutAnimationEndEvent(modifier, cb) {
|
||||
const menu = this.getMenu();
|
||||
const duration = menu ? this._getAnimationDuration(menu) + 50 : 0;
|
||||
|
||||
if (!duration) {
|
||||
modifier.call(this);
|
||||
cb.call(this);
|
||||
return;
|
||||
}
|
||||
|
||||
this._transitionCallback = (e) => {
|
||||
if (e.target !== menu) return;
|
||||
this._unbindLayoutAnimationEndEvent();
|
||||
cb.call(this);
|
||||
};
|
||||
|
||||
TRANS_EVENTS.forEach((e) => {
|
||||
menu.addEventListener(e, this._transitionCallback, false);
|
||||
});
|
||||
|
||||
modifier.call(this);
|
||||
|
||||
this._transitionCallbackTimeout = setTimeout(() => {
|
||||
this._transitionCallback.call(this, { target: menu });
|
||||
}, duration);
|
||||
},
|
||||
|
||||
// ---
|
||||
// Remove layout sivenav toggle animationEnd event
|
||||
_unbindLayoutAnimationEndEvent() {
|
||||
const menu = this.getMenu();
|
||||
|
||||
if (this._transitionCallbackTimeout) {
|
||||
clearTimeout(this._transitionCallbackTimeout);
|
||||
this._transitionCallbackTimeout = null;
|
||||
}
|
||||
|
||||
if (menu && this._transitionCallback) {
|
||||
TRANS_EVENTS.forEach((e) => {
|
||||
menu.removeEventListener(e, this._transitionCallback, false);
|
||||
});
|
||||
}
|
||||
|
||||
if (this._transitionCallback) {
|
||||
this._transitionCallback = null;
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Bind delayed window resize event
|
||||
_bindWindowResizeEvent() {
|
||||
this._unbindWindowResizeEvent();
|
||||
|
||||
const cb = () => {
|
||||
if (this._resizeTimeout) {
|
||||
clearTimeout(this._resizeTimeout);
|
||||
this._resizeTimeout = null;
|
||||
}
|
||||
this._triggerEvent("resize");
|
||||
};
|
||||
|
||||
this._resizeCallback = () => {
|
||||
if (this._resizeTimeout) clearTimeout(this._resizeTimeout);
|
||||
this._resizeTimeout = setTimeout(cb, this.RESIZE_DELAY);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", this._resizeCallback, false);
|
||||
},
|
||||
|
||||
// ---
|
||||
// Unbind delayed window resize event
|
||||
_unbindWindowResizeEvent() {
|
||||
if (this._resizeTimeout) {
|
||||
clearTimeout(this._resizeTimeout);
|
||||
this._resizeTimeout = null;
|
||||
}
|
||||
|
||||
if (this._resizeCallback) {
|
||||
window.removeEventListener("resize", this._resizeCallback, false);
|
||||
this._resizeCallback = null;
|
||||
}
|
||||
},
|
||||
|
||||
_bindMenuMouseEvents() {
|
||||
if (this._menuMouseEnter && this._menuMouseLeave && this._windowTouchStart)
|
||||
return;
|
||||
|
||||
const layoutMenu = this.getLayoutMenu();
|
||||
if (!layoutMenu) return this._unbindMenuMouseEvents();
|
||||
|
||||
if (!this._menuMouseEnter) {
|
||||
this._menuMouseEnter = () => {
|
||||
if (this.isSmallScreen() || this._hasClass("layout-transitioning")) {
|
||||
return this._setMenuHoverState(false);
|
||||
}
|
||||
|
||||
return this._setMenuHoverState(false);
|
||||
};
|
||||
layoutMenu.addEventListener("mouseenter", this._menuMouseEnter, false);
|
||||
layoutMenu.addEventListener("touchstart", this._menuMouseEnter, false);
|
||||
}
|
||||
|
||||
if (!this._menuMouseLeave) {
|
||||
this._menuMouseLeave = () => {
|
||||
this._setMenuHoverState(false);
|
||||
};
|
||||
layoutMenu.addEventListener("mouseleave", this._menuMouseLeave, false);
|
||||
}
|
||||
|
||||
if (!this._windowTouchStart) {
|
||||
this._windowTouchStart = (e) => {
|
||||
if (!e || !e.target || !this._findParent(e.target, ".layout-menu")) {
|
||||
this._setMenuHoverState(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("touchstart", this._windowTouchStart, true);
|
||||
}
|
||||
},
|
||||
|
||||
_unbindMenuMouseEvents() {
|
||||
if (
|
||||
!this._menuMouseEnter &&
|
||||
!this._menuMouseLeave &&
|
||||
!this._windowTouchStart
|
||||
)
|
||||
return;
|
||||
|
||||
const layoutMenu = this.getLayoutMenu();
|
||||
|
||||
if (this._menuMouseEnter) {
|
||||
if (layoutMenu) {
|
||||
layoutMenu.removeEventListener(
|
||||
"mouseenter",
|
||||
this._menuMouseEnter,
|
||||
false,
|
||||
);
|
||||
layoutMenu.removeEventListener(
|
||||
"touchstart",
|
||||
this._menuMouseEnter,
|
||||
false,
|
||||
);
|
||||
}
|
||||
this._menuMouseEnter = null;
|
||||
}
|
||||
|
||||
if (this._menuMouseLeave) {
|
||||
if (layoutMenu) {
|
||||
layoutMenu.removeEventListener(
|
||||
"mouseleave",
|
||||
this._menuMouseLeave,
|
||||
false,
|
||||
);
|
||||
}
|
||||
this._menuMouseLeave = null;
|
||||
}
|
||||
|
||||
if (this._windowTouchStart) {
|
||||
if (layoutMenu) {
|
||||
window.addEventListener("touchstart", this._windowTouchStart, true);
|
||||
}
|
||||
this._windowTouchStart = null;
|
||||
}
|
||||
|
||||
this._setMenuHoverState(false);
|
||||
},
|
||||
|
||||
// *******************************************************************************
|
||||
// * Methods
|
||||
|
||||
scrollToActive(animate = false) {
|
||||
this._scrollToActive(animate);
|
||||
},
|
||||
|
||||
// ---
|
||||
// Collapse / expand layout
|
||||
setCollapsed(collapsed = requiredParam("collapsed"), animate = true) {
|
||||
const layoutMenu = this.getLayoutMenu();
|
||||
|
||||
if (!layoutMenu) return;
|
||||
|
||||
this._unbindLayoutAnimationEndEvent();
|
||||
|
||||
if (animate && this._supportsTransitionEnd()) {
|
||||
this._addClass("layout-transitioning");
|
||||
if (collapsed) this._setMenuHoverState(false);
|
||||
|
||||
this._bindLayoutAnimationEndEvent(
|
||||
() => {
|
||||
// Collapse / Expand
|
||||
if (this.isSmallScreen) this._setCollapsed(collapsed);
|
||||
},
|
||||
() => {
|
||||
this._removeClass("layout-transitioning");
|
||||
this._triggerWindowEvent("resize");
|
||||
this._triggerEvent("toggle");
|
||||
this._setMenuHoverState(false);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this._addClass("layout-no-transition");
|
||||
if (collapsed) this._setMenuHoverState(false);
|
||||
|
||||
// Collapse / Expand
|
||||
this._setCollapsed(collapsed);
|
||||
|
||||
setTimeout(() => {
|
||||
this._removeClass("layout-no-transition");
|
||||
this._triggerWindowEvent("resize");
|
||||
this._triggerEvent("toggle");
|
||||
this._setMenuHoverState(false);
|
||||
}, 1);
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Toggle layout
|
||||
toggleCollapsed(animate = true) {
|
||||
this.setCollapsed(!this.isCollapsed(), animate);
|
||||
},
|
||||
|
||||
// ---
|
||||
// Set layout positioning
|
||||
setPosition(
|
||||
fixed = requiredParam("fixed"),
|
||||
offcanvas = requiredParam("offcanvas"),
|
||||
) {
|
||||
this._removeClass(
|
||||
"layout-menu-offcanvas layout-menu-fixed layout-menu-fixed-offcanvas",
|
||||
);
|
||||
|
||||
if (!fixed && offcanvas) {
|
||||
this._addClass("layout-menu-offcanvas");
|
||||
} else if (fixed && !offcanvas) {
|
||||
this._addClass("layout-menu-fixed");
|
||||
this._redrawLayoutMenu();
|
||||
} else if (fixed && offcanvas) {
|
||||
this._addClass("layout-menu-fixed-offcanvas");
|
||||
this._redrawLayoutMenu();
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
// *******************************************************************************
|
||||
// * Getters
|
||||
|
||||
getLayoutMenu() {
|
||||
return document.querySelector(".layout-menu");
|
||||
},
|
||||
|
||||
getMenu() {
|
||||
const layoutMenu = this.getLayoutMenu();
|
||||
|
||||
if (!layoutMenu) return null;
|
||||
|
||||
return !this._hasClass("menu", layoutMenu)
|
||||
? layoutMenu.querySelector(".menu")
|
||||
: layoutMenu;
|
||||
},
|
||||
|
||||
getLayoutNavbar() {
|
||||
return document.querySelector(".layout-navbar");
|
||||
},
|
||||
|
||||
getLayoutFooter() {
|
||||
return document.querySelector(".content-footer");
|
||||
},
|
||||
|
||||
// *******************************************************************************
|
||||
// * Update
|
||||
|
||||
update() {
|
||||
if (
|
||||
(this.getLayoutNavbar() &&
|
||||
((!this.isSmallScreen() &&
|
||||
this.isLayoutNavbarFull() &&
|
||||
this.isFixed()) ||
|
||||
this.isNavbarFixed())) ||
|
||||
(this.getLayoutFooter() && this.isFooterFixed())
|
||||
) {
|
||||
this._updateInlineStyle(this._getNavbarHeight(), this._getFooterHeight());
|
||||
}
|
||||
|
||||
this._bindMenuMouseEvents();
|
||||
},
|
||||
|
||||
setAutoUpdate(enable = requiredParam("enable")) {
|
||||
if (enable && !this._autoUpdate) {
|
||||
this.on("resize.Helpers:autoUpdate", () => this.update());
|
||||
this._autoUpdate = true;
|
||||
} else if (!enable && this._autoUpdate) {
|
||||
this.off("resize.Helpers:autoUpdate");
|
||||
this._autoUpdate = false;
|
||||
}
|
||||
},
|
||||
|
||||
// *******************************************************************************
|
||||
// * Tests
|
||||
|
||||
isRtl() {
|
||||
return (
|
||||
document.querySelector("body").getAttribute("dir") === "rtl" ||
|
||||
document.querySelector("html").getAttribute("dir") === "rtl"
|
||||
);
|
||||
},
|
||||
|
||||
isMobileDevice() {
|
||||
return (
|
||||
typeof window.orientation !== "undefined" ||
|
||||
navigator.userAgent.indexOf("IEMobile") !== -1
|
||||
);
|
||||
},
|
||||
|
||||
isSmallScreen() {
|
||||
return (
|
||||
(window.innerWidth ||
|
||||
document.documentElement.clientWidth ||
|
||||
document.body.clientWidth) < this.LAYOUT_BREAKPOINT
|
||||
);
|
||||
},
|
||||
|
||||
isLayoutNavbarFull() {
|
||||
return !!document.querySelector(".layout-wrapper.layout-navbar-full");
|
||||
},
|
||||
|
||||
isCollapsed() {
|
||||
if (this.isSmallScreen()) {
|
||||
return !this._hasClass("layout-menu-expanded");
|
||||
}
|
||||
return this._hasClass("layout-menu-collapsed");
|
||||
},
|
||||
|
||||
isFixed() {
|
||||
return this._hasClass("layout-menu-fixed layout-menu-fixed-offcanvas");
|
||||
},
|
||||
|
||||
isNavbarFixed() {
|
||||
return (
|
||||
this._hasClass("layout-navbar-fixed") ||
|
||||
(!this.isSmallScreen() && this.isFixed() && this.isLayoutNavbarFull())
|
||||
);
|
||||
},
|
||||
|
||||
isFooterFixed() {
|
||||
return this._hasClass("layout-footer-fixed");
|
||||
},
|
||||
|
||||
isLightStyle() {
|
||||
return document.documentElement.classList.contains("light-style");
|
||||
},
|
||||
|
||||
// *******************************************************************************
|
||||
// * Events
|
||||
|
||||
on(event = requiredParam("event"), callback = requiredParam("callback")) {
|
||||
const [_event] = event.split(".");
|
||||
let [, ...namespace] = event.split(".");
|
||||
// let [_event, ...namespace] = event.split('.')
|
||||
namespace = namespace.join(".") || null;
|
||||
|
||||
this._listeners.push({ event: _event, namespace, callback });
|
||||
},
|
||||
|
||||
off(event = requiredParam("event")) {
|
||||
const [_event] = event.split(".");
|
||||
let [, ...namespace] = event.split(".");
|
||||
namespace = namespace.join(".") || null;
|
||||
|
||||
this._listeners
|
||||
.filter(
|
||||
(listener) =>
|
||||
listener.event === _event && listener.namespace === namespace,
|
||||
)
|
||||
.forEach((listener) =>
|
||||
this._listeners.splice(this._listeners.indexOf(listener), 1),
|
||||
);
|
||||
},
|
||||
|
||||
// *******************************************************************************
|
||||
// * Life cycle
|
||||
|
||||
init() {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
|
||||
// Initialize `style` element
|
||||
this._updateInlineStyle(0);
|
||||
|
||||
// Bind window resize event
|
||||
this._bindWindowResizeEvent();
|
||||
|
||||
// Bind init event
|
||||
this.off("init._Helpers");
|
||||
this.on("init._Helpers", () => {
|
||||
this.off("resize._Helpers:redrawMenu");
|
||||
this.on("resize._Helpers:redrawMenu", () => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this.isSmallScreen() && !this.isCollapsed() && this._redrawLayoutMenu();
|
||||
});
|
||||
|
||||
// Force repaint in IE 10
|
||||
if (
|
||||
typeof document.documentMode === "number" &&
|
||||
document.documentMode < 11
|
||||
) {
|
||||
this.off("resize._Helpers:ie10RepaintBody");
|
||||
this.on("resize._Helpers:ie10RepaintBody", () => {
|
||||
if (this.isFixed()) return;
|
||||
const { scrollTop } = document.documentElement;
|
||||
document.body.style.display = "none";
|
||||
// document.body.offsetHeight
|
||||
document.body.style.display = "block";
|
||||
document.documentElement.scrollTop = scrollTop;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this._triggerEvent("init");
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (!this._initialized) return;
|
||||
this._initialized = false;
|
||||
|
||||
this._removeClass("layout-transitioning");
|
||||
this._removeInlineStyle();
|
||||
this._unbindLayoutAnimationEndEvent();
|
||||
this._unbindWindowResizeEvent();
|
||||
this._unbindMenuMouseEvents();
|
||||
this.setAutoUpdate(false);
|
||||
|
||||
this.off("init._Helpers");
|
||||
|
||||
// Remove all listeners except `init`
|
||||
this._listeners
|
||||
.filter((listener) => listener.event !== "init")
|
||||
.forEach((listener) =>
|
||||
this._listeners.splice(this._listeners.indexOf(listener), 1),
|
||||
);
|
||||
},
|
||||
|
||||
// ---
|
||||
// Init Password Toggle
|
||||
initPasswordToggle() {
|
||||
const toggler = document.querySelectorAll(
|
||||
".form-password-toggle i:not(.copy-to-clipboard)",
|
||||
);
|
||||
if (typeof toggler !== "undefined" && toggler !== null) {
|
||||
toggler.forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const formPasswordToggle = el.closest(".form-password-toggle");
|
||||
const formPasswordToggleInput =
|
||||
formPasswordToggle.querySelector("input");
|
||||
|
||||
if (formPasswordToggleInput.getAttribute("type") === "text") {
|
||||
formPasswordToggleInput.setAttribute("type", "password");
|
||||
formPasswordToggle
|
||||
.querySelector("i.bx-show")
|
||||
.classList.replace("bx-show", "bx-hide");
|
||||
} else if (
|
||||
formPasswordToggleInput.getAttribute("type") === "password"
|
||||
) {
|
||||
formPasswordToggleInput.setAttribute("type", "text");
|
||||
formPasswordToggle
|
||||
.querySelector("i.bx-hide")
|
||||
.classList.replace("bx-hide", "bx-show");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
// Init Speech To Text
|
||||
initSpeechToText() {
|
||||
const SpeechRecognition =
|
||||
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const speechToText = document.querySelectorAll(".speech-to-text");
|
||||
if (SpeechRecognition !== undefined && SpeechRecognition !== null) {
|
||||
if (typeof speechToText !== "undefined" && speechToText !== null) {
|
||||
const recognition = new SpeechRecognition();
|
||||
const toggler = document.querySelectorAll(".speech-to-text i");
|
||||
toggler.forEach((el) => {
|
||||
let listening = false;
|
||||
el.addEventListener("click", () => {
|
||||
el.closest(".input-group").querySelector(".form-control").focus();
|
||||
recognition.onspeechstart = () => {
|
||||
listening = true;
|
||||
};
|
||||
if (listening === false) {
|
||||
recognition.start();
|
||||
}
|
||||
recognition.onerror = () => {
|
||||
listening = false;
|
||||
};
|
||||
recognition.onresult = (event) => {
|
||||
el.closest(".input-group").querySelector(".form-control").value =
|
||||
event.results[0][0].transcript;
|
||||
};
|
||||
recognition.onspeechend = () => {
|
||||
listening = false;
|
||||
recognition.stop();
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Ajax Call Promise
|
||||
ajaxCall(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = new XMLHttpRequest();
|
||||
req.open("GET", url);
|
||||
req.onload = () =>
|
||||
req.status === 200
|
||||
? resolve(req.response)
|
||||
: reject(Error(req.statusText));
|
||||
req.onerror = (e) => reject(Error(`Network Error: ${e}`));
|
||||
req.send();
|
||||
});
|
||||
},
|
||||
|
||||
// ---
|
||||
// SidebarToggle (Used in Apps)
|
||||
initSidebarToggle() {
|
||||
const sidebarToggler = document.querySelectorAll(
|
||||
'[data-bs-toggle="sidebar"]',
|
||||
);
|
||||
|
||||
sidebarToggler.forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const target = el.getAttribute("data-target");
|
||||
const overlay = el.getAttribute("data-overlay");
|
||||
const appOverlay = document.querySelectorAll(".app-overlay");
|
||||
const targetEl = document.querySelectorAll(target);
|
||||
|
||||
targetEl.forEach((tel) => {
|
||||
tel.classList.toggle("show");
|
||||
if (
|
||||
typeof overlay !== "undefined" &&
|
||||
overlay !== null &&
|
||||
overlay !== false &&
|
||||
typeof appOverlay !== "undefined"
|
||||
) {
|
||||
if (tel.classList.contains("show")) {
|
||||
appOverlay[0].classList.add("show");
|
||||
} else {
|
||||
appOverlay[0].classList.remove("show");
|
||||
}
|
||||
appOverlay[0].addEventListener("click", (e) => {
|
||||
e.currentTarget.classList.remove("show");
|
||||
tel.classList.remove("show");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// *******************************************************************************
|
||||
// * Initialization
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Helpers.init();
|
||||
|
||||
if (Helpers.isMobileDevice() && window.chrome) {
|
||||
document.documentElement.classList.add("layout-menu-100vh");
|
||||
}
|
||||
|
||||
// Update layout after page load
|
||||
if (document.readyState === "complete") Helpers.update();
|
||||
else
|
||||
document.addEventListener("DOMContentLoaded", function onContentLoaded() {
|
||||
Helpers.update();
|
||||
document.removeEventListener("DOMContentLoaded", onContentLoaded);
|
||||
});
|
||||
}
|
||||
|
||||
// ---
|
||||
window.Helpers = Helpers;
|
||||
322
src/ui/app/static/js/main.js
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
/**
|
||||
* Main
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
let menu, animate;
|
||||
|
||||
(function () {
|
||||
// Initialize menu
|
||||
//-----------------
|
||||
|
||||
let layoutMenuEl = document.querySelectorAll("#layout-menu");
|
||||
layoutMenuEl.forEach(function (element) {
|
||||
menu = new Menu(element, {
|
||||
orientation: "vertical",
|
||||
closeChildren: false,
|
||||
});
|
||||
// Change parameter to true if you want scroll animation
|
||||
window.Helpers.scrollToActive((animate = false));
|
||||
window.Helpers.mainMenu = menu;
|
||||
});
|
||||
|
||||
// Initialize menu togglers and bind click on each
|
||||
let menuToggler = document.querySelectorAll(".layout-menu-toggle");
|
||||
menuToggler.forEach((item) => {
|
||||
item.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
window.Helpers.toggleCollapsed();
|
||||
});
|
||||
});
|
||||
|
||||
// Display menu toggle (layout-menu-toggle) on hover with delay
|
||||
let delay = function (elem, callback) {
|
||||
let timeout = null;
|
||||
elem.onmouseenter = function () {
|
||||
// Set timeout to be a timer which will invoke callback after 300ms (not for small screen)
|
||||
if (!Helpers.isSmallScreen()) {
|
||||
timeout = setTimeout(callback, 300);
|
||||
} else {
|
||||
timeout = setTimeout(callback, 0);
|
||||
}
|
||||
};
|
||||
|
||||
elem.onmouseleave = function () {
|
||||
// Clear any timers set to timeout
|
||||
document.querySelector(".layout-menu-toggle").classList.remove("d-block");
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
};
|
||||
if (document.getElementById("layout-menu")) {
|
||||
delay(document.getElementById("layout-menu"), function () {
|
||||
// not for small screen
|
||||
if (!Helpers.isSmallScreen()) {
|
||||
document.querySelector(".layout-menu-toggle").classList.add("d-block");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Display in main menu when menu scrolls
|
||||
let menuInnerContainer = document.getElementsByClassName("menu-inner"),
|
||||
menuInnerShadow = document.getElementsByClassName("menu-inner-shadow")[0];
|
||||
if (menuInnerContainer.length > 0 && menuInnerShadow) {
|
||||
menuInnerContainer[0].addEventListener("ps-scroll-y", function () {
|
||||
if (this.querySelector(".ps__thumb-y").offsetTop) {
|
||||
menuInnerShadow.style.display = "block";
|
||||
} else {
|
||||
menuInnerShadow.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Init helpers & misc
|
||||
// --------------------
|
||||
|
||||
// Init BS Tooltip
|
||||
const tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Accordion active class
|
||||
const accordionActiveFunction = function (e) {
|
||||
if (e.type == "show.bs.collapse" || e.type == "show.bs.collapse") {
|
||||
e.target.closest(".accordion-item").classList.add("active");
|
||||
} else {
|
||||
e.target.closest(".accordion-item").classList.remove("active");
|
||||
}
|
||||
};
|
||||
|
||||
const accordionTriggerList = [].slice.call(
|
||||
document.querySelectorAll(".accordion"),
|
||||
);
|
||||
const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
|
||||
accordionTriggerEl.addEventListener(
|
||||
"show.bs.collapse",
|
||||
accordionActiveFunction,
|
||||
);
|
||||
accordionTriggerEl.addEventListener(
|
||||
"hide.bs.collapse",
|
||||
accordionActiveFunction,
|
||||
);
|
||||
});
|
||||
|
||||
// Auto update layout based on screen size
|
||||
window.Helpers.setAutoUpdate(true);
|
||||
|
||||
// Toggle Password Visibility
|
||||
window.Helpers.initPasswordToggle();
|
||||
|
||||
// Speech To Text
|
||||
window.Helpers.initSpeechToText();
|
||||
|
||||
// Manage menu expanded/collapsed with templateCustomizer & local storage
|
||||
//------------------------------------------------------------------
|
||||
|
||||
// If current layout is horizontal OR current window screen is small (overlay menu) than return from here
|
||||
if (window.Helpers.isSmallScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If current layout is vertical and current window screen is > small
|
||||
|
||||
// Auto update menu collapsed/expanded based on the themeConfig
|
||||
window.Helpers.setCollapsed(true, false);
|
||||
})();
|
||||
|
||||
/**
|
||||
* Custom
|
||||
*/
|
||||
|
||||
class News {
|
||||
constructor() {
|
||||
this.BASE_URL = "https://www.bunkerweb.io/";
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("load", () => {
|
||||
if (sessionStorage.getItem("lastRefetch") !== null) {
|
||||
const storeStamp = sessionStorage.getItem("lastRefetch");
|
||||
const nowStamp = Math.round(new Date().getTime() / 1000);
|
||||
if (+nowStamp > storeStamp) {
|
||||
sessionStorage.removeItem("lastRefetch");
|
||||
sessionStorage.removeItem("lastNews");
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionStorage.getItem("lastNews") !== null)
|
||||
return this.render(JSON.parse(sessionStorage.getItem("lastNews")));
|
||||
|
||||
fetch("https://www.bunkerweb.io/api/posts/0/2")
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => {
|
||||
const reverseData = res.data.reverse();
|
||||
return this.render(reverseData);
|
||||
})
|
||||
.catch((e) => {});
|
||||
});
|
||||
}
|
||||
|
||||
render(lastNews) {
|
||||
// store for next time if not the case
|
||||
if (
|
||||
!sessionStorage.getItem("lastNews") &&
|
||||
!sessionStorage.getItem("lastRefetch")
|
||||
) {
|
||||
sessionStorage.setItem(
|
||||
"lastRefetch",
|
||||
Math.round(new Date().getTime() / 1000) + 3600,
|
||||
);
|
||||
sessionStorage.setItem("lastNews", JSON.stringify(lastNews));
|
||||
const newsNumber = lastNews.length;
|
||||
document.querySelector("#news-pill").insertAdjacentHTML(
|
||||
"beforeend",
|
||||
DOMPurify.sanitize(`<span class="badge rounded-pill badge-center-sm bg-danger ms-1_5"
|
||||
>${newsNumber}</span
|
||||
>`),
|
||||
);
|
||||
document.querySelector("#news-button").insertAdjacentHTML(
|
||||
"beforeend",
|
||||
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>`),
|
||||
);
|
||||
}
|
||||
|
||||
const newsContainer = document.querySelector("[data-news-container]");
|
||||
const lastItem = lastNews[0];
|
||||
//remove default message
|
||||
newsContainer.textContent = "";
|
||||
document
|
||||
.querySelector("[data-news-container]")
|
||||
.insertAdjacentHTML(
|
||||
"afterbegin",
|
||||
`<div data-news-row class="row g-6 justify-content-center">`,
|
||||
);
|
||||
//render last news
|
||||
lastNews.forEach((news) => {
|
||||
//create html card from infos
|
||||
const cardHTML = this.template(
|
||||
news.title,
|
||||
news.slug,
|
||||
news.photo.url,
|
||||
news.excerpt,
|
||||
news.tags,
|
||||
news.date,
|
||||
news === lastItem,
|
||||
);
|
||||
const BASE_URL = this.BASE_URL;
|
||||
let cleanHTML = DOMPurify.sanitize(cardHTML);
|
||||
//add to DOM inside the created div
|
||||
document
|
||||
.querySelector("[data-news-row]")
|
||||
.insertAdjacentHTML("afterbegin", cleanHTML);
|
||||
document.querySelectorAll(`.blog-click-${news.slug}`).forEach((slug) => {
|
||||
slug.addEventListener("click", function () {
|
||||
window.open(
|
||||
`${BASE_URL}blog/post/${news.slug}?utm_campaign=self&utm_source=ui`,
|
||||
"_blank",
|
||||
);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".blog-click-tag").forEach((tag) => {
|
||||
tag.target = "_blank";
|
||||
});
|
||||
});
|
||||
document
|
||||
.querySelector("[data-news-container]")
|
||||
.insertAdjacentHTML("beforeend", "</div>");
|
||||
}
|
||||
|
||||
template(title, slug, img, excerpt, tags, date, last) {
|
||||
//loop on tags to get list
|
||||
let tagList = "";
|
||||
tags.forEach((tag) => {
|
||||
tagList += `<a
|
||||
role="button"
|
||||
href="${this.BASE_URL}/blog/tag/${tag.slug}?utm_campaign=self&utm_source=ui"
|
||||
aria-pressed="true"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span class="tf-icons bx bx-xs bx-purchase-tag bx-18px me-2"></span
|
||||
>${tag.name}
|
||||
</a>
|
||||
`;
|
||||
});
|
||||
const card = `<div class="col-md-11 col-xl-11 ${last ? "" : "mb-1"}">
|
||||
<div class="card">
|
||||
<a
|
||||
href="${this.BASE_URL}blog/post/${slug}?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
><img class="card-img-top" src="${img}" alt="News image"
|
||||
/></a>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<a
|
||||
href="${
|
||||
this.BASE_URL
|
||||
}blog/post/${slug}?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>${title}</a
|
||||
>
|
||||
</h5>
|
||||
<p class="card-text">${excerpt}</p>
|
||||
<p class="d-flex flex-wrap">${tagList}</p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">Posted on : ${date}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
const setNews = new News();
|
||||
|
||||
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
|
||||
// set all elements owning target to target=_blank
|
||||
if ("target" in node) {
|
||||
node.setAttribute("target", "_blank");
|
||||
node.setAttribute("rel", "noopener");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function onContentLoaded() {
|
||||
setNews.init();
|
||||
|
||||
// Generic Copy to Clipboard with Tooltip
|
||||
$(".copy-to-clipboard").on("click", function () {
|
||||
const input = $(this).closest(".input-group").find("input")[0];
|
||||
|
||||
// Use the Clipboard API
|
||||
navigator.clipboard
|
||||
.writeText(input.value)
|
||||
.then(() => {
|
||||
// Show tooltip
|
||||
const button = $(this);
|
||||
button.attr("data-bs-original-title", "Copied!").tooltip("show");
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
button.tooltip("hide").attr("data-bs-original-title", "");
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy text: ", err);
|
||||
});
|
||||
});
|
||||
});
|
||||
653
src/ui/app/static/js/menu.js
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
const TRANSITION_EVENTS = [
|
||||
"transitionend",
|
||||
"webkitTransitionEnd",
|
||||
"oTransitionEnd",
|
||||
];
|
||||
// const TRANSITION_PROPERTIES = ['transition', 'MozTransition', 'webkitTransition', 'WebkitTransition', 'OTransition']
|
||||
|
||||
class Menu {
|
||||
constructor(el, config = {}, _PS = null) {
|
||||
this._el = el;
|
||||
this._animate = config.animate !== false;
|
||||
this._accordion = config.accordion !== false;
|
||||
this._closeChildren = Boolean(config.closeChildren);
|
||||
|
||||
this._onOpen = config.onOpen || (() => {});
|
||||
this._onOpened = config.onOpened || (() => {});
|
||||
this._onClose = config.onClose || (() => {});
|
||||
this._onClosed = config.onClosed || (() => {});
|
||||
|
||||
this._psScroll = null;
|
||||
this._topParent = null;
|
||||
this._menuBgClass = null;
|
||||
|
||||
el.classList.add("menu");
|
||||
el.classList[this._animate ? "remove" : "add"]("menu-no-animation"); // check
|
||||
|
||||
el.classList.add("menu-vertical");
|
||||
|
||||
const PerfectScrollbarLib = _PS || window.PerfectScrollbar;
|
||||
|
||||
if (PerfectScrollbarLib) {
|
||||
this._scrollbar = new PerfectScrollbarLib(
|
||||
el.querySelector(".menu-inner"),
|
||||
{
|
||||
suppressScrollX: true,
|
||||
wheelPropagation: !Menu._hasClass(
|
||||
"layout-menu-fixed layout-menu-fixed-offcanvas",
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
window.Helpers.menuPsScroll = this._scrollbar;
|
||||
} else {
|
||||
el.querySelector(".menu-inner").classList.add("overflow-auto");
|
||||
}
|
||||
|
||||
// Add data attribute for bg color class of menu
|
||||
const menuClassList = el.classList;
|
||||
|
||||
for (let i = 0; i < menuClassList.length; i++) {
|
||||
if (menuClassList[i].startsWith("bg-")) {
|
||||
this._menuBgClass = menuClassList[i];
|
||||
}
|
||||
}
|
||||
el.setAttribute("data-bg-class", this._menuBgClass);
|
||||
|
||||
this._bindEvents();
|
||||
|
||||
// Link menu instance to element
|
||||
el.menuInstance = this;
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
// Click Event
|
||||
this._evntElClick = (e) => {
|
||||
// Find top parent element
|
||||
if (
|
||||
e.target.closest("ul") &&
|
||||
e.target.closest("ul").classList.contains("menu-inner")
|
||||
) {
|
||||
const menuItem = Menu._findParent(e.target, "menu-item", false);
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
if (menuItem) this._topParent = menuItem.childNodes[0];
|
||||
}
|
||||
|
||||
const toggleLink = e.target.classList.contains("menu-toggle")
|
||||
? e.target
|
||||
: Menu._findParent(e.target, "menu-toggle", false);
|
||||
|
||||
if (toggleLink) {
|
||||
e.preventDefault();
|
||||
|
||||
if (toggleLink.getAttribute("data-hover") !== "true") {
|
||||
this.toggle(toggleLink);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (window.Helpers.isMobileDevice)
|
||||
this._el.addEventListener("click", this._evntElClick);
|
||||
|
||||
this._evntWindowResize = () => {
|
||||
this.update();
|
||||
if (this._lastWidth !== window.innerWidth) {
|
||||
this._lastWidth = window.innerWidth;
|
||||
this.update();
|
||||
}
|
||||
|
||||
const horizontalMenuTemplate = document.querySelector(
|
||||
"[data-template^='horizontal-menu']",
|
||||
);
|
||||
if (!this._horizontal && !horizontalMenuTemplate) this.manageScroll();
|
||||
};
|
||||
window.addEventListener("resize", this._evntWindowResize);
|
||||
}
|
||||
|
||||
static childOf(/* child node */ c, /* parent node */ p) {
|
||||
// returns boolean
|
||||
if (c.parentNode) {
|
||||
while ((c = c.parentNode) && c !== p);
|
||||
return !!c;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_unbindEvents() {
|
||||
if (this._evntElClick) {
|
||||
this._el.removeEventListener("click", this._evntElClick);
|
||||
this._evntElClick = null;
|
||||
}
|
||||
|
||||
if (this._evntElMouseOver) {
|
||||
this._el.removeEventListener("mouseover", this._evntElMouseOver);
|
||||
this._evntElMouseOver = null;
|
||||
}
|
||||
|
||||
if (this._evntElMouseOut) {
|
||||
this._el.removeEventListener("mouseout", this._evntElMouseOut);
|
||||
this._evntElMouseOut = null;
|
||||
}
|
||||
|
||||
if (this._evntWindowResize) {
|
||||
window.removeEventListener("resize", this._evntWindowResize);
|
||||
this._evntWindowResize = null;
|
||||
}
|
||||
|
||||
if (this._evntBodyClick) {
|
||||
document.body.removeEventListener("click", this._evntBodyClick);
|
||||
this._evntBodyClick = null;
|
||||
}
|
||||
|
||||
if (this._evntInnerMousemove) {
|
||||
this._inner.removeEventListener("mousemove", this._evntInnerMousemove);
|
||||
this._evntInnerMousemove = null;
|
||||
}
|
||||
|
||||
if (this._evntInnerMouseleave) {
|
||||
this._inner.removeEventListener("mouseleave", this._evntInnerMouseleave);
|
||||
this._evntInnerMouseleave = null;
|
||||
}
|
||||
}
|
||||
|
||||
static _isRoot(item) {
|
||||
return !Menu._findParent(item, "menu-item", false);
|
||||
}
|
||||
|
||||
static _findParent(el, cls, throwError = true) {
|
||||
if (el.tagName.toUpperCase() === "BODY") return null;
|
||||
el = el.parentNode;
|
||||
while (el.tagName.toUpperCase() !== "BODY" && !el.classList.contains(cls)) {
|
||||
el = el.parentNode;
|
||||
}
|
||||
|
||||
el = el.tagName.toUpperCase() !== "BODY" ? el : null;
|
||||
|
||||
if (!el && throwError)
|
||||
throw new Error(`Cannot find \`.${cls}\` parent element`);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
static _findChild(el, cls) {
|
||||
const items = el.childNodes;
|
||||
const found = [];
|
||||
|
||||
for (let i = 0, l = items.length; i < l; i++) {
|
||||
if (items[i].classList) {
|
||||
let passed = 0;
|
||||
|
||||
for (let j = 0; j < cls.length; j++) {
|
||||
if (items[i].classList.contains(cls[j])) passed += 1;
|
||||
}
|
||||
|
||||
if (cls.length === passed) found.push(items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
static _findMenu(item) {
|
||||
let curEl = item.childNodes[0];
|
||||
let menu = null;
|
||||
|
||||
while (curEl && !menu) {
|
||||
if (curEl.classList && curEl.classList.contains("menu-sub")) menu = curEl;
|
||||
curEl = curEl.nextSibling;
|
||||
}
|
||||
|
||||
if (!menu)
|
||||
throw new Error(
|
||||
"Cannot find `.menu-sub` element for the current `.menu-toggle`",
|
||||
);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Has class
|
||||
static _hasClass(cls, el = window.Helpers.ROOT_EL) {
|
||||
let result = false;
|
||||
|
||||
cls.split(" ").forEach((c) => {
|
||||
if (el.classList.contains(c)) result = true;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
open(el, closeChildren = this._closeChildren) {
|
||||
const item = this._findUnopenedParent(
|
||||
Menu._getItem(el, true),
|
||||
closeChildren,
|
||||
);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
const toggleLink = Menu._getLink(item, true);
|
||||
|
||||
Menu._promisify(this._onOpen, this, item, toggleLink, Menu._findMenu(item))
|
||||
.then(() => {
|
||||
if (!this._horizontal || !Menu._isRoot(item)) {
|
||||
if (this._animate && !this._horizontal) {
|
||||
window.requestAnimationFrame(() =>
|
||||
this._toggleAnimation(true, item, false),
|
||||
);
|
||||
if (this._accordion) this._closeOther(item, closeChildren);
|
||||
} else if (this._animate) {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this._onOpened &&
|
||||
this._onOpened(this, item, toggleLink, Menu._findMenu(item));
|
||||
} else {
|
||||
item.classList.add("open");
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this._onOpened &&
|
||||
this._onOpened(this, item, toggleLink, Menu._findMenu(item));
|
||||
if (this._accordion) this._closeOther(item, closeChildren);
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this._onOpened &&
|
||||
this._onOpened(this, item, toggleLink, Menu._findMenu(item));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
close(el, closeChildren = this._closeChildren, _autoClose = false) {
|
||||
const item = Menu._getItem(el, true);
|
||||
const toggleLink = Menu._getLink(el, true);
|
||||
|
||||
if (!item.classList.contains("open") || item.classList.contains("disabled"))
|
||||
return;
|
||||
|
||||
Menu._promisify(
|
||||
this._onClose,
|
||||
this,
|
||||
item,
|
||||
toggleLink,
|
||||
Menu._findMenu(item),
|
||||
_autoClose,
|
||||
)
|
||||
.then(() => {
|
||||
if (!this._horizontal || !Menu._isRoot(item)) {
|
||||
if (this._animate && !this._horizontal) {
|
||||
window.requestAnimationFrame(() =>
|
||||
this._toggleAnimation(false, item, closeChildren),
|
||||
);
|
||||
} else {
|
||||
item.classList.remove("open");
|
||||
|
||||
if (closeChildren) {
|
||||
const opened = item.querySelectorAll(".menu-item.open");
|
||||
for (let i = 0, l = opened.length; i < l; i++)
|
||||
opened[i].classList.remove("open");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this._onClosed &&
|
||||
this._onClosed(this, item, toggleLink, Menu._findMenu(item));
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this._onClosed &&
|
||||
this._onClosed(this, item, toggleLink, Menu._findMenu(item));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
_closeOther(item, closeChildren) {
|
||||
const opened = Menu._findChild(item.parentNode, ["menu-item", "open"]);
|
||||
|
||||
for (let i = 0, l = opened.length; i < l; i++) {
|
||||
if (opened[i] !== item) this.close(opened[i], closeChildren);
|
||||
}
|
||||
}
|
||||
|
||||
toggle(el, closeChildren = this._closeChildren) {
|
||||
const item = Menu._getItem(el, true);
|
||||
// const toggleLink = Menu._getLink(el, true)
|
||||
|
||||
if (item.classList.contains("open")) this.close(item, closeChildren);
|
||||
else this.open(item, closeChildren);
|
||||
}
|
||||
|
||||
static _getItem(el, toggle) {
|
||||
let item = null;
|
||||
const selector = toggle ? "menu-toggle" : "menu-link";
|
||||
|
||||
if (el.classList.contains("menu-item")) {
|
||||
if (Menu._findChild(el, [selector]).length) item = el;
|
||||
} else if (el.classList.contains(selector)) {
|
||||
item = el.parentNode.classList.contains("menu-item")
|
||||
? el.parentNode
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
throw new Error(
|
||||
`${toggle ? "Toggable " : ""}\`.menu-item\` element not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
static _getLink(el, toggle) {
|
||||
let found = [];
|
||||
const selector = toggle ? "menu-toggle" : "menu-link";
|
||||
|
||||
if (el.classList.contains(selector)) found = [el];
|
||||
else if (el.classList.contains("menu-item"))
|
||||
found = Menu._findChild(el, [selector]);
|
||||
|
||||
if (!found.length) throw new Error(`\`${selector}\` element not found.`);
|
||||
|
||||
return found[0];
|
||||
}
|
||||
|
||||
_findUnopenedParent(item, closeChildren) {
|
||||
let tree = [];
|
||||
let parentItem = null;
|
||||
|
||||
while (item) {
|
||||
if (item.classList.contains("disabled")) {
|
||||
parentItem = null;
|
||||
tree = [];
|
||||
} else {
|
||||
if (!item.classList.contains("open")) parentItem = item;
|
||||
tree.push(item);
|
||||
}
|
||||
|
||||
item = Menu._findParent(item, "menu-item", false);
|
||||
}
|
||||
|
||||
if (!parentItem) return null;
|
||||
if (tree.length === 1) return parentItem;
|
||||
|
||||
tree = tree.slice(0, tree.indexOf(parentItem));
|
||||
|
||||
for (let i = 0, l = tree.length; i < l; i++) {
|
||||
tree[i].classList.add("open");
|
||||
|
||||
if (this._accordion) {
|
||||
const openedItems = Menu._findChild(tree[i].parentNode, [
|
||||
"menu-item",
|
||||
"open",
|
||||
]);
|
||||
|
||||
for (let j = 0, k = openedItems.length; j < k; j++) {
|
||||
if (openedItems[j] !== tree[i]) {
|
||||
openedItems[j].classList.remove("open");
|
||||
|
||||
if (closeChildren) {
|
||||
const openedChildren =
|
||||
openedItems[j].querySelectorAll(".menu-item.open");
|
||||
for (let x = 0, z = openedChildren.length; x < z; x++) {
|
||||
openedChildren[x].classList.remove("open");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parentItem;
|
||||
}
|
||||
|
||||
_toggleAnimation(open, item, closeChildren) {
|
||||
const toggleLink = Menu._getLink(item, true);
|
||||
const menu = Menu._findMenu(item);
|
||||
|
||||
Menu._unbindAnimationEndEvent(item);
|
||||
|
||||
const linkHeight = Math.round(toggleLink.getBoundingClientRect().height);
|
||||
|
||||
item.style.overflow = "hidden";
|
||||
|
||||
const clearItemStyle = () => {
|
||||
item.classList.remove("menu-item-animating");
|
||||
item.classList.remove("menu-item-closing");
|
||||
item.style.overflow = null;
|
||||
item.style.height = null;
|
||||
|
||||
this.update();
|
||||
};
|
||||
|
||||
if (open) {
|
||||
item.style.height = `${linkHeight}px`;
|
||||
item.classList.add("menu-item-animating");
|
||||
item.classList.add("open");
|
||||
|
||||
Menu._bindAnimationEndEvent(item, () => {
|
||||
clearItemStyle();
|
||||
this._onOpened(this, item, toggleLink, menu);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
item.style.height = `${
|
||||
linkHeight + Math.round(menu.getBoundingClientRect().height)
|
||||
}px`;
|
||||
}, 50);
|
||||
} else {
|
||||
item.style.height = `${
|
||||
linkHeight + Math.round(menu.getBoundingClientRect().height)
|
||||
}px`;
|
||||
item.classList.add("menu-item-animating");
|
||||
item.classList.add("menu-item-closing");
|
||||
|
||||
Menu._bindAnimationEndEvent(item, () => {
|
||||
item.classList.remove("open");
|
||||
clearItemStyle();
|
||||
|
||||
if (closeChildren) {
|
||||
const opened = item.querySelectorAll(".menu-item.open");
|
||||
for (let i = 0, l = opened.length; i < l; i++)
|
||||
opened[i].classList.remove("open");
|
||||
}
|
||||
|
||||
this._onClosed(this, item, toggleLink, menu);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
item.style.height = `${linkHeight}px`;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
static _bindAnimationEndEvent(el, handler) {
|
||||
const cb = (e) => {
|
||||
if (e.target !== el) return;
|
||||
Menu._unbindAnimationEndEvent(el);
|
||||
handler(e);
|
||||
};
|
||||
|
||||
let duration = window.getComputedStyle(el).transitionDuration;
|
||||
duration =
|
||||
parseFloat(duration) * (duration.indexOf("ms") !== -1 ? 1 : 1000);
|
||||
|
||||
el._menuAnimationEndEventCb = cb;
|
||||
TRANSITION_EVENTS.forEach((ev) =>
|
||||
el.addEventListener(ev, el._menuAnimationEndEventCb, false),
|
||||
);
|
||||
|
||||
el._menuAnimationEndEventTimeout = setTimeout(() => {
|
||||
cb({ target: el });
|
||||
}, duration + 50);
|
||||
}
|
||||
|
||||
_getItemOffset(item) {
|
||||
let curItem = this._inner.childNodes[0];
|
||||
let left = 0;
|
||||
|
||||
while (curItem !== item) {
|
||||
if (curItem.tagName) {
|
||||
left += Math.round(curItem.getBoundingClientRect().width);
|
||||
}
|
||||
|
||||
curItem = curItem.nextSibling;
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
static _promisify(fn, ...args) {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
return result;
|
||||
}
|
||||
if (result === false) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
get _innerWidth() {
|
||||
const items = this._inner.childNodes;
|
||||
let width = 0;
|
||||
|
||||
for (let i = 0, l = items.length; i < l; i++) {
|
||||
if (items[i].tagName) {
|
||||
width += Math.round(items[i].getBoundingClientRect().width);
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
get _innerPosition() {
|
||||
return parseInt(
|
||||
this._inner.style[this._rtl ? "marginRight" : "marginLeft"] || "0px",
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
set _innerPosition(value) {
|
||||
this._inner.style[this._rtl ? "marginRight" : "marginLeft"] = `${value}px`;
|
||||
return value;
|
||||
}
|
||||
|
||||
static _unbindAnimationEndEvent(el) {
|
||||
const cb = el._menuAnimationEndEventCb;
|
||||
|
||||
if (el._menuAnimationEndEventTimeout) {
|
||||
clearTimeout(el._menuAnimationEndEventTimeout);
|
||||
el._menuAnimationEndEventTimeout = null;
|
||||
}
|
||||
|
||||
if (!cb) return;
|
||||
|
||||
TRANSITION_EVENTS.forEach((ev) => el.removeEventListener(ev, cb, false));
|
||||
el._menuAnimationEndEventCb = null;
|
||||
}
|
||||
|
||||
closeAll(closeChildren = this._closeChildren) {
|
||||
const opened = this._el.querySelectorAll(".menu-inner > .menu-item.open");
|
||||
|
||||
for (let i = 0, l = opened.length; i < l; i++)
|
||||
this.close(opened[i], closeChildren);
|
||||
}
|
||||
|
||||
static setDisabled(el, disabled) {
|
||||
Menu._getItem(el, false).classList[disabled ? "add" : "remove"]("disabled");
|
||||
}
|
||||
|
||||
static isActive(el) {
|
||||
return Menu._getItem(el, false).classList.contains("active");
|
||||
}
|
||||
|
||||
static isOpened(el) {
|
||||
return Menu._getItem(el, false).classList.contains("open");
|
||||
}
|
||||
|
||||
static isDisabled(el) {
|
||||
return Menu._getItem(el, false).classList.contains("disabled");
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this._scrollbar) {
|
||||
this._scrollbar.update();
|
||||
}
|
||||
}
|
||||
|
||||
manageScroll() {
|
||||
const { PerfectScrollbar } = window;
|
||||
const menuInner = document.querySelector(".menu-inner");
|
||||
|
||||
if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
|
||||
if (this._scrollbar !== null) {
|
||||
// window.Helpers.menuPsScroll.destroy()
|
||||
this._scrollbar.destroy();
|
||||
this._scrollbar = null;
|
||||
}
|
||||
menuInner.classList.add("overflow-auto");
|
||||
} else {
|
||||
if (this._scrollbar === null) {
|
||||
const menuScroll = new PerfectScrollbar(
|
||||
document.querySelector(".menu-inner"),
|
||||
{
|
||||
suppressScrollX: true,
|
||||
wheelPropagation: false,
|
||||
},
|
||||
);
|
||||
this._scrollbar = menuScroll;
|
||||
}
|
||||
menuInner.classList.remove("overflow-auto");
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this._el) return;
|
||||
|
||||
this._unbindEvents();
|
||||
|
||||
const items = this._el.querySelectorAll(".menu-item");
|
||||
for (let i = 0, l = items.length; i < l; i++) {
|
||||
Menu._unbindAnimationEndEvent(items[i]);
|
||||
items[i].classList.remove("menu-item-animating");
|
||||
items[i].classList.remove("open");
|
||||
items[i].style.overflow = null;
|
||||
items[i].style.height = null;
|
||||
}
|
||||
|
||||
const menus = this._el.querySelectorAll(".menu-menu");
|
||||
for (let i2 = 0, l2 = menus.length; i2 < l2; i2++) {
|
||||
menus[i2].style.marginRight = null;
|
||||
menus[i2].style.marginLeft = null;
|
||||
}
|
||||
|
||||
this._el.classList.remove("menu-no-animation");
|
||||
|
||||
if (this._wrapper) {
|
||||
this._prevBtn.parentNode.removeChild(this._prevBtn);
|
||||
this._nextBtn.parentNode.removeChild(this._nextBtn);
|
||||
this._wrapper.parentNode.insertBefore(this._inner, this._wrapper);
|
||||
this._wrapper.parentNode.removeChild(this._wrapper);
|
||||
this._inner.style.marginLeft = null;
|
||||
this._inner.style.marginRight = null;
|
||||
}
|
||||
|
||||
this._el.menuInstance = null;
|
||||
delete this._el.menuInstance;
|
||||
|
||||
this._el = null;
|
||||
this._animate = null;
|
||||
this._accordion = null;
|
||||
this._closeChildren = null;
|
||||
this._onOpen = null;
|
||||
this._onOpened = null;
|
||||
this._onClose = null;
|
||||
this._onClosed = null;
|
||||
if (this._scrollbar) {
|
||||
this._scrollbar.destroy();
|
||||
this._scrollbar = null;
|
||||
}
|
||||
this._inner = null;
|
||||
this._prevBtn = null;
|
||||
this._wrapper = null;
|
||||
this._nextBtn = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.Menu = Menu;
|
||||
29
src/ui/app/static/js/pages-account-settings-account.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Account Settings - Account
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function (e) {
|
||||
(function () {
|
||||
const deactivateAcc = document.querySelector("#formAccountDeactivation");
|
||||
|
||||
// Update/reset user image of account page
|
||||
let accountUserImage = document.getElementById("uploadedAvatar");
|
||||
const fileInput = document.querySelector(".account-file-input"),
|
||||
resetFileInput = document.querySelector(".account-image-reset");
|
||||
|
||||
if (accountUserImage) {
|
||||
const resetImage = accountUserImage.src;
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files[0]) {
|
||||
accountUserImage.src = window.URL.createObjectURL(fileInput.files[0]);
|
||||
}
|
||||
};
|
||||
resetFileInput.onclick = () => {
|
||||
fileInput.value = "";
|
||||
accountUserImage.src = resetImage;
|
||||
};
|
||||
}
|
||||
})();
|
||||
});
|
||||
149
src/ui/app/static/js/pages/profile.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
$(document).ready(function () {
|
||||
function validatePassword() {
|
||||
const password = $("#new_password").val();
|
||||
let isValid = true;
|
||||
|
||||
// Validate length
|
||||
if (password.length >= 8) {
|
||||
$("#length-check i")
|
||||
.removeClass("bx-x text-danger")
|
||||
.addClass("bx-check text-success");
|
||||
} else {
|
||||
isValid = false;
|
||||
$("#length-check i")
|
||||
.removeClass("bx-check text-success")
|
||||
.addClass("bx-x text-danger");
|
||||
}
|
||||
|
||||
// Validate uppercase letter
|
||||
if (/[A-Z]/.test(password)) {
|
||||
$("#uppercase-check i")
|
||||
.removeClass("bx-x text-danger")
|
||||
.addClass("bx-check text-success");
|
||||
} else {
|
||||
isValid = false;
|
||||
$("#uppercase-check i")
|
||||
.removeClass("bx-check text-success")
|
||||
.addClass("bx-x text-danger");
|
||||
}
|
||||
|
||||
// Validate number
|
||||
if (/\d/.test(password)) {
|
||||
$("#number-check i")
|
||||
.removeClass("bx-x text-danger")
|
||||
.addClass("bx-check text-success");
|
||||
} else {
|
||||
isValid = false;
|
||||
$("#number-check i")
|
||||
.removeClass("bx-check text-success")
|
||||
.addClass("bx-x text-danger");
|
||||
}
|
||||
|
||||
// Validate special character
|
||||
if (/[ !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(password)) {
|
||||
$("#special-check i")
|
||||
.removeClass("bx-x text-danger")
|
||||
.addClass("bx-check text-success");
|
||||
} else {
|
||||
isValid = false;
|
||||
$("#special-check i")
|
||||
.removeClass("bx-check text-success")
|
||||
.addClass("bx-x text-danger");
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Real-time validation as user types
|
||||
$("#new_password").on("input", function () {
|
||||
if (validatePassword()) {
|
||||
$(this).removeClass("is-invalid");
|
||||
$(this).addClass("is-valid");
|
||||
}
|
||||
});
|
||||
|
||||
function matchPassword() {
|
||||
const newPassword = $("#new_password").val();
|
||||
const confirmPassword = $("#new_password_confirm").val();
|
||||
|
||||
if (newPassword === confirmPassword) {
|
||||
$("#new_password_confirm").removeClass("is-invalid");
|
||||
$("#new_password_confirm").addClass("is-valid");
|
||||
} else {
|
||||
$("#new_password_confirm").removeClass("is-valid");
|
||||
$("#new_password_confirm").addClass("is-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
$("#new_password_confirm").on("input", function () {
|
||||
if (matchPassword()) {
|
||||
$(this).removeClass("is-invalid");
|
||||
$(this).addClass("is-valid");
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission validation
|
||||
$("#formPasswordSettings").on("submit", function (e) {
|
||||
const newPasswordInput = $("#new_password");
|
||||
const confirmPasswordInput = $("#new_password_confirm");
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Check if passwords match
|
||||
if (newPasswordInput.val() !== confirmPasswordInput.val()) {
|
||||
isValid = false;
|
||||
confirmPasswordInput.addClass("is-invalid");
|
||||
} else {
|
||||
confirmPasswordInput.removeClass("is-invalid");
|
||||
confirmPasswordInput.addClass("is-valid");
|
||||
}
|
||||
|
||||
// Validate password using real-time checks
|
||||
if (!validatePassword()) {
|
||||
isValid = false;
|
||||
newPasswordInput.addClass("is-invalid");
|
||||
} else {
|
||||
newPasswordInput.removeClass("is-invalid");
|
||||
newPasswordInput.addClass("is-valid");
|
||||
}
|
||||
|
||||
// Prevent form submission if validation fails
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for tab change
|
||||
$('button[data-bs-toggle="tab"]').on("shown.bs.tab", function (e) {
|
||||
// Get the target tab's ID (data-bs-target without the '#')
|
||||
var target = $(e.target)
|
||||
.data("bs-target")
|
||||
.substring(1)
|
||||
.replace("navs-pills-", "");
|
||||
|
||||
if (target === "profile") {
|
||||
if (window.location.hash) {
|
||||
history.pushState(
|
||||
"",
|
||||
document.title,
|
||||
window.location.pathname + window.location.search,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the URL fragment
|
||||
window.location.hash = target;
|
||||
});
|
||||
|
||||
// On page load, activate the tab based on the URL fragment
|
||||
var hash = window.location.hash;
|
||||
if (hash) {
|
||||
var targetTab = $(
|
||||
`button[data-bs-target="#navs-pills-${hash.substring(1)}"]`,
|
||||
);
|
||||
if (targetTab.length) {
|
||||
targetTab.tab("show");
|
||||
}
|
||||
}
|
||||
});
|
||||
35
src/ui/app/static/js/ui-modals.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* UI Modals
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
// On hiding modal, remove iframe video/audio to stop playing
|
||||
const youTubeModal = document.querySelector("#youTubeModal"),
|
||||
youTubeModalVideo = youTubeModal.querySelector("iframe");
|
||||
youTubeModal.addEventListener("hidden.bs.modal", function () {
|
||||
youTubeModalVideo.setAttribute("src", "");
|
||||
});
|
||||
|
||||
// Function to get and auto play youTube video
|
||||
const autoPlayYouTubeModal = function () {
|
||||
const modalTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="modal"]'),
|
||||
);
|
||||
modalTriggerList.map(function (modalTriggerEl) {
|
||||
modalTriggerEl.onclick = function () {
|
||||
const theModal = this.getAttribute("data-bs-target"),
|
||||
videoSRC = this.getAttribute("data-theVideo"),
|
||||
videoSRCauto = `${videoSRC}?autoplay=1`,
|
||||
modalVideo = document.querySelector(`${theModal} iframe`);
|
||||
if (modalVideo) {
|
||||
modalVideo.setAttribute("src", videoSRCauto);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Calling function on load
|
||||
autoPlayYouTubeModal();
|
||||
})();
|
||||
18
src/ui/app/static/js/ui-popover.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// /**
|
||||
// * UI Tooltips & Popovers
|
||||
// */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
const popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||
);
|
||||
const popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
// added { html: true, sanitize: false } option to render button in content area of popover
|
||||
return new bootstrap.Popover(popoverTriggerEl, {
|
||||
html: true,
|
||||
sanitize: false,
|
||||
});
|
||||
});
|
||||
})();
|
||||
47
src/ui/app/static/js/ui-toasts.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* UI Toasts
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
// Bootstrap toasts example
|
||||
// --------------------------------------------------------------------
|
||||
const toastPlacementExample = document.querySelector(".toast-placement-ex"),
|
||||
toastPlacementBtn = document.querySelector("#showToastPlacement");
|
||||
let selectedType, selectedPlacement, toastPlacement;
|
||||
|
||||
// Dispose toast when open another
|
||||
function toastDispose(toast) {
|
||||
if (toast && toast._element !== null) {
|
||||
if (toastPlacementExample) {
|
||||
toastPlacementExample.classList.remove(selectedType);
|
||||
DOMTokenList.prototype.remove.apply(
|
||||
toastPlacementExample.classList,
|
||||
selectedPlacement,
|
||||
);
|
||||
}
|
||||
toast.dispose();
|
||||
}
|
||||
}
|
||||
// Placement Button click
|
||||
if (toastPlacementBtn) {
|
||||
toastPlacementBtn.onclick = function () {
|
||||
if (toastPlacement) {
|
||||
toastDispose(toastPlacement);
|
||||
}
|
||||
selectedType = document.querySelector("#selectTypeOpt").value;
|
||||
selectedPlacement = document
|
||||
.querySelector("#selectPlacement")
|
||||
.value.split(" ");
|
||||
|
||||
toastPlacementExample.classList.add(selectedType);
|
||||
DOMTokenList.prototype.add.apply(
|
||||
toastPlacementExample.classList,
|
||||
selectedPlacement,
|
||||
);
|
||||
toastPlacement = new bootstrap.Toast(toastPlacementExample);
|
||||
toastPlacement.show();
|
||||
};
|
||||
}
|
||||
})();
|
||||
689
src/ui/app/static/libs/apexcharts/apexcharts.css
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
@keyframes opaque {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resizeanim {
|
||||
0%,
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-canvas {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.apexcharts-canvas ::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.apexcharts-canvas ::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.apexcharts-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.apexcharts-text tspan {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
rect.legend-mouseover-inactive,
|
||||
.legend-mouseover-inactive rect,
|
||||
.legend-mouseover-inactive path,
|
||||
.legend-mouseover-inactive circle,
|
||||
.legend-mouseover-inactive line,
|
||||
.legend-mouseover-inactive text.apexcharts-yaxis-title-text,
|
||||
.legend-mouseover-inactive text.apexcharts-yaxis-label {
|
||||
transition: 0.15s ease all;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.apexcharts-legend-text {
|
||||
padding-left: 15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.apexcharts-series-collapsed {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip {
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 2px 6px -4px #999;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
left: 62px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
z-index: 12;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-active {
|
||||
opacity: 1;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-theme-light {
|
||||
border: 1px solid #e3e3e3;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-theme-dark {
|
||||
color: #fff;
|
||||
background: rgba(30, 30, 30, 0.8);
|
||||
}
|
||||
|
||||
.apexcharts-tooltip * {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
padding: 6px;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
|
||||
background: #eceff1;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-goals-value,
|
||||
.apexcharts-tooltip-text-y-value,
|
||||
.apexcharts-tooltip-text-z-value {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-goals-label:empty,
|
||||
.apexcharts-tooltip-text-goals-value:empty,
|
||||
.apexcharts-tooltip-text-y-label:empty,
|
||||
.apexcharts-tooltip-text-y-value:empty,
|
||||
.apexcharts-tooltip-text-z-value:empty,
|
||||
.apexcharts-tooltip-title:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-goals-label,
|
||||
.apexcharts-tooltip-text-goals-value {
|
||||
padding: 6px 0 5px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-goals-group,
|
||||
.apexcharts-tooltip-text-goals-label,
|
||||
.apexcharts-tooltip-text-goals-value {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-goals-label:not(:empty),
|
||||
.apexcharts-tooltip-text-goals-value:not(:empty) {
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group {
|
||||
padding: 0 10px;
|
||||
display: none;
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group.apexcharts-active,
|
||||
.apexcharts-tooltip-series-group:last-child {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-y-group {
|
||||
padding: 6px 0 5px;
|
||||
}
|
||||
|
||||
.apexcharts-custom-tooltip,
|
||||
.apexcharts-tooltip-box {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-boxPlot {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-box > div {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-box span.value {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-rangebar {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-rangebar .category {
|
||||
font-weight: 600;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-rangebar .series-name {
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip,
|
||||
.apexcharts-yaxistooltip {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
color: #373d3f;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: #eceff1;
|
||||
border: 1px solid #90a4ae;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip {
|
||||
padding: 9px 10px;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip.apexcharts-theme-dark {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip:after,
|
||||
.apexcharts-xaxistooltip:before {
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip:after {
|
||||
border-color: transparent;
|
||||
border-width: 6px;
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip:before {
|
||||
border-color: transparent;
|
||||
border-width: 7px;
|
||||
margin-left: -7px;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-bottom:after,
|
||||
.apexcharts-xaxistooltip-bottom:before {
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-top:after,
|
||||
.apexcharts-xaxistooltip-top:before {
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-bottom:after {
|
||||
border-bottom-color: #eceff1;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-bottom:before {
|
||||
border-bottom-color: #90a4ae;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after,
|
||||
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {
|
||||
border-bottom-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-top:after {
|
||||
border-top-color: #eceff1;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-top:before {
|
||||
border-top-color: #90a4ae;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after,
|
||||
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {
|
||||
border-top-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip.apexcharts-active {
|
||||
opacity: 1;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip.apexcharts-theme-dark {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip:after,
|
||||
.apexcharts-yaxistooltip:before {
|
||||
top: 50%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip:after {
|
||||
border-color: transparent;
|
||||
border-width: 6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip:before {
|
||||
border-color: transparent;
|
||||
border-width: 7px;
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-left:after,
|
||||
.apexcharts-yaxistooltip-left:before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-right:after,
|
||||
.apexcharts-yaxistooltip-right:before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-left:after {
|
||||
border-left-color: #eceff1;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-left:before {
|
||||
border-left-color: #90a4ae;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after,
|
||||
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {
|
||||
border-left-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-right:after {
|
||||
border-right-color: #eceff1;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-right:before {
|
||||
border-right-color: #90a4ae;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after,
|
||||
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {
|
||||
border-right-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip.apexcharts-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apexcharts-xcrosshairs,
|
||||
.apexcharts-ycrosshairs {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-xcrosshairs.apexcharts-active,
|
||||
.apexcharts-ycrosshairs.apexcharts-active {
|
||||
opacity: 1;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-ycrosshairs-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.apexcharts-selection-rect {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.svg_select_boundingRect,
|
||||
.svg_select_points_rot {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.apexcharts-selection-rect + g .svg_select_boundingRect,
|
||||
.apexcharts-selection-rect + g .svg_select_points_rot {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.apexcharts-selection-rect + g .svg_select_points_l,
|
||||
.apexcharts-selection-rect + g .svg_select_points_r {
|
||||
cursor: ew-resize;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.svg_select_points {
|
||||
fill: #efefef;
|
||||
stroke: #333;
|
||||
rx: 2;
|
||||
}
|
||||
|
||||
.apexcharts-svg.apexcharts-zoomable.hovering-zoom {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.apexcharts-svg.apexcharts-zoomable.hovering-pan {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.apexcharts-menu-icon,
|
||||
.apexcharts-pan-icon,
|
||||
.apexcharts-reset-icon,
|
||||
.apexcharts-selection-icon,
|
||||
.apexcharts-toolbar-custom-icon,
|
||||
.apexcharts-zoom-icon,
|
||||
.apexcharts-zoomin-icon,
|
||||
.apexcharts-zoomout-icon {
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 24px;
|
||||
color: #6e8192;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.apexcharts-menu-icon svg,
|
||||
.apexcharts-reset-icon svg,
|
||||
.apexcharts-zoom-icon svg,
|
||||
.apexcharts-zoomin-icon svg,
|
||||
.apexcharts-zoomout-icon svg {
|
||||
fill: #6e8192;
|
||||
}
|
||||
|
||||
.apexcharts-selection-icon svg {
|
||||
fill: #444;
|
||||
transform: scale(0.76);
|
||||
}
|
||||
|
||||
.apexcharts-theme-dark .apexcharts-menu-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-pan-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-reset-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-selection-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-zoom-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-zoomin-icon svg,
|
||||
.apexcharts-theme-dark .apexcharts-zoomout-icon svg {
|
||||
fill: #f3f4f5;
|
||||
}
|
||||
|
||||
.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg,
|
||||
.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,
|
||||
.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg {
|
||||
fill: #008ffb;
|
||||
}
|
||||
|
||||
.apexcharts-theme-light .apexcharts-menu-icon:hover svg,
|
||||
.apexcharts-theme-light .apexcharts-reset-icon:hover svg,
|
||||
.apexcharts-theme-light
|
||||
.apexcharts-selection-icon:not(.apexcharts-selected):hover
|
||||
svg,
|
||||
.apexcharts-theme-light
|
||||
.apexcharts-zoom-icon:not(.apexcharts-selected):hover
|
||||
svg,
|
||||
.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,
|
||||
.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg {
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
.apexcharts-menu-icon,
|
||||
.apexcharts-selection-icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.apexcharts-reset-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.apexcharts-menu-icon,
|
||||
.apexcharts-reset-icon,
|
||||
.apexcharts-zoom-icon {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.apexcharts-zoomin-icon,
|
||||
.apexcharts-zoomout-icon {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
.apexcharts-zoomout-icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.apexcharts-pan-icon {
|
||||
transform: scale(0.62);
|
||||
position: relative;
|
||||
left: 1px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.apexcharts-pan-icon svg {
|
||||
fill: #fff;
|
||||
stroke: #6e8192;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.apexcharts-pan-icon.apexcharts-selected svg {
|
||||
stroke: #008ffb;
|
||||
}
|
||||
|
||||
.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
.apexcharts-toolbar {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
max-width: 176px;
|
||||
text-align: right;
|
||||
border-radius: 3px;
|
||||
padding: 0 6px 2px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.apexcharts-menu {
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
right: 10px;
|
||||
opacity: 0;
|
||||
min-width: 110px;
|
||||
transition: 0.15s ease all;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-menu.apexcharts-menu-open {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-menu-item {
|
||||
padding: 6px 7px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.apexcharts-theme-light .apexcharts-menu-item:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.apexcharts-theme-dark .apexcharts-menu {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.apexcharts-canvas:hover .apexcharts-toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-canvas .apexcharts-element-hidden,
|
||||
.apexcharts-datalabel.apexcharts-element-hidden,
|
||||
.apexcharts-hide .apexcharts-series-points {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.apexcharts-hidden-element-shown {
|
||||
opacity: 1;
|
||||
transition: 0.25s ease all;
|
||||
}
|
||||
|
||||
.apexcharts-datalabel,
|
||||
.apexcharts-datalabel-label,
|
||||
.apexcharts-datalabel-value,
|
||||
.apexcharts-datalabels,
|
||||
.apexcharts-pie-label {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-pie-label-delay {
|
||||
opacity: 0;
|
||||
animation-name: opaque;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease;
|
||||
}
|
||||
|
||||
.apexcharts-radialbar-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.apexcharts-annotation-rect,
|
||||
.apexcharts-area-series .apexcharts-area,
|
||||
.apexcharts-area-series
|
||||
.apexcharts-series-markers
|
||||
.apexcharts-marker.no-pointer-events,
|
||||
.apexcharts-gridline,
|
||||
.apexcharts-line,
|
||||
.apexcharts-line-series
|
||||
.apexcharts-series-markers
|
||||
.apexcharts-marker.no-pointer-events,
|
||||
.apexcharts-point-annotation-label,
|
||||
.apexcharts-radar-series path:not(.apexcharts-marker),
|
||||
.apexcharts-radar-series polygon,
|
||||
.apexcharts-toolbar svg,
|
||||
.apexcharts-tooltip .apexcharts-marker,
|
||||
.apexcharts-xaxis-annotation-label,
|
||||
.apexcharts-yaxis-annotation-label,
|
||||
.apexcharts-zoom-rect {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-active .apexcharts-marker {
|
||||
transition: 0.15s ease all;
|
||||
}
|
||||
|
||||
.resize-triggers {
|
||||
animation: 1ms resizeanim;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contract-trigger:before,
|
||||
.resize-triggers,
|
||||
.resize-triggers > div {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.resize-triggers > div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #eee;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.contract-trigger:before {
|
||||
overflow: hidden;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
}
|
||||
|
||||
.apexcharts-bar-goals-markers {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-bar-shadows {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.apexcharts-rangebar-goals-markers {
|
||||
pointer-events: none;
|
||||
}
|
||||
14
src/ui/app/static/libs/apexcharts/apexcharts.min.js
vendored
Normal file
55
src/ui/app/static/libs/apexcharts/locales/ar.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "ar",
|
||||
"options": {
|
||||
"months": [
|
||||
"يناير",
|
||||
"فبراير",
|
||||
"مارس",
|
||||
"أبريل",
|
||||
"مايو",
|
||||
"يونيو",
|
||||
"يوليو",
|
||||
"أغسطس",
|
||||
"سبتمبر",
|
||||
"أكتوبر",
|
||||
"نوفمبر",
|
||||
"ديسمبر"
|
||||
],
|
||||
"shortMonths": [
|
||||
"يناير",
|
||||
"فبراير",
|
||||
"مارس",
|
||||
"أبريل",
|
||||
"مايو",
|
||||
"يونيو",
|
||||
"يوليو",
|
||||
"أغسطس",
|
||||
"سبتمبر",
|
||||
"أكتوبر",
|
||||
"نوفمبر",
|
||||
"ديسمبر"
|
||||
],
|
||||
"days": [
|
||||
"الأحد",
|
||||
"الإثنين",
|
||||
"الثلاثاء",
|
||||
"الأربعاء",
|
||||
"الخميس",
|
||||
"الجمعة",
|
||||
"السبت"
|
||||
],
|
||||
"shortDays": ["أحد", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "تحميل بصيغة SVG",
|
||||
"exportToPNG": "تحميل بصيغة PNG",
|
||||
"exportToCSV": "تحميل بصيغة CSV",
|
||||
"menu": "القائمة",
|
||||
"selection": "تحديد",
|
||||
"selectionZoom": "تكبير التحديد",
|
||||
"zoomIn": "تكبير",
|
||||
"zoomOut": "تصغير",
|
||||
"pan": "تحريك",
|
||||
"reset": "إعادة التعيين"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/be-cyrl.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "be-cyrl",
|
||||
"options": {
|
||||
"months": [
|
||||
"Студзень",
|
||||
"Люты",
|
||||
"Сакавік",
|
||||
"Красавік",
|
||||
"Травень",
|
||||
"Чэрвень",
|
||||
"Ліпень",
|
||||
"Жнівень",
|
||||
"Верасень",
|
||||
"Кастрычнік",
|
||||
"Лістапад",
|
||||
"Сьнежань"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Сту",
|
||||
"Лют",
|
||||
"Сак",
|
||||
"Кра",
|
||||
"Тра",
|
||||
"Чэр",
|
||||
"Ліп",
|
||||
"Жні",
|
||||
"Вер",
|
||||
"Кас",
|
||||
"Ліс",
|
||||
"Сьн"
|
||||
],
|
||||
"days": [
|
||||
"Нядзеля",
|
||||
"Панядзелак",
|
||||
"Аўторак",
|
||||
"Серада",
|
||||
"Чацьвер",
|
||||
"Пятніца",
|
||||
"Субота"
|
||||
],
|
||||
"shortDays": ["Нд", "Пн", "Аў", "Ср", "Чц", "Пт", "Сб"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Спампаваць SVG",
|
||||
"exportToPNG": "Спампаваць PNG",
|
||||
"exportToCSV": "Спампаваць CSV",
|
||||
"menu": "Мэню",
|
||||
"selection": "Вылучэньне",
|
||||
"selectionZoom": "Вылучэньне з маштабаваньнем",
|
||||
"zoomIn": "Наблізіць",
|
||||
"zoomOut": "Аддаліць",
|
||||
"pan": "Ссоўваньне",
|
||||
"reset": "Скінуць маштабаваньне"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/be-latn.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "be-latn",
|
||||
"options": {
|
||||
"months": [
|
||||
"Studzień",
|
||||
"Luty",
|
||||
"Sakavik",
|
||||
"Krasavik",
|
||||
"Travień",
|
||||
"Červień",
|
||||
"Lipień",
|
||||
"Žnivień",
|
||||
"Vierasień",
|
||||
"Kastryčnik",
|
||||
"Listapad",
|
||||
"Śniežań"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Stu",
|
||||
"Lut",
|
||||
"Sak",
|
||||
"Kra",
|
||||
"Tra",
|
||||
"Čer",
|
||||
"Lip",
|
||||
"Žni",
|
||||
"Vie",
|
||||
"Kas",
|
||||
"Lis",
|
||||
"Śni"
|
||||
],
|
||||
"days": [
|
||||
"Niadziela",
|
||||
"Paniadziełak",
|
||||
"Aŭtorak",
|
||||
"Sierada",
|
||||
"Čaćvier",
|
||||
"Piatnica",
|
||||
"Subota"
|
||||
],
|
||||
"shortDays": ["Nd", "Pn", "Aŭ", "Sr", "Čć", "Pt", "Sb"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Spampavać SVG",
|
||||
"exportToPNG": "Spampavać PNG",
|
||||
"exportToCSV": "Spampavać CSV",
|
||||
"menu": "Meniu",
|
||||
"selection": "Vyłučeńnie",
|
||||
"selectionZoom": "Vyłučeńnie z maštabavańniem",
|
||||
"zoomIn": "Nablizić",
|
||||
"zoomOut": "Addalić",
|
||||
"pan": "Ssoŭvańnie",
|
||||
"reset": "Skinuć maštabavańnie"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/ca.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "ca",
|
||||
"options": {
|
||||
"months": [
|
||||
"Gener",
|
||||
"Febrer",
|
||||
"Març",
|
||||
"Abril",
|
||||
"Maig",
|
||||
"Juny",
|
||||
"Juliol",
|
||||
"Agost",
|
||||
"Setembre",
|
||||
"Octubre",
|
||||
"Novembre",
|
||||
"Desembre"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Gen.",
|
||||
"Febr.",
|
||||
"Març",
|
||||
"Abr.",
|
||||
"Maig",
|
||||
"Juny",
|
||||
"Jul.",
|
||||
"Ag.",
|
||||
"Set.",
|
||||
"Oct.",
|
||||
"Nov.",
|
||||
"Des."
|
||||
],
|
||||
"days": [
|
||||
"Diumenge",
|
||||
"Dilluns",
|
||||
"Dimarts",
|
||||
"Dimecres",
|
||||
"Dijous",
|
||||
"Divendres",
|
||||
"Dissabte"
|
||||
],
|
||||
"shortDays": ["Dg", "Dl", "Dt", "Dc", "Dj", "Dv", "Ds"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Descarregar SVG",
|
||||
"exportToPNG": "Descarregar PNG",
|
||||
"exportToCSV": "Descarregar CSV",
|
||||
"menu": "Menú",
|
||||
"selection": "Seleccionar",
|
||||
"selectionZoom": "Seleccionar Zoom",
|
||||
"zoomIn": "Augmentar",
|
||||
"zoomOut": "Disminuir",
|
||||
"pan": "Navegació",
|
||||
"reset": "Reiniciar Zoom"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/cs.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "cs",
|
||||
"options": {
|
||||
"months": [
|
||||
"Leden",
|
||||
"Únor",
|
||||
"Březen",
|
||||
"Duben",
|
||||
"Květen",
|
||||
"Červen",
|
||||
"Červenec",
|
||||
"Srpen",
|
||||
"Září",
|
||||
"Říjen",
|
||||
"Listopad",
|
||||
"Prosinec"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Led",
|
||||
"Úno",
|
||||
"Bře",
|
||||
"Dub",
|
||||
"Kvě",
|
||||
"Čvn",
|
||||
"Čvc",
|
||||
"Srp",
|
||||
"Zář",
|
||||
"Říj",
|
||||
"Lis",
|
||||
"Pro"
|
||||
],
|
||||
"days": [
|
||||
"Neděle",
|
||||
"Pondělí",
|
||||
"Úterý",
|
||||
"Středa",
|
||||
"Čtvrtek",
|
||||
"Pátek",
|
||||
"Sobota"
|
||||
],
|
||||
"shortDays": ["Ne", "Po", "Út", "St", "Čt", "Pá", "So"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Stáhnout SVG",
|
||||
"exportToPNG": "Stáhnout PNG",
|
||||
"exportToCSV": "Stáhnout CSV",
|
||||
"menu": "Menu",
|
||||
"selection": "Vybrat",
|
||||
"selectionZoom": "Zoom: Vybrat",
|
||||
"zoomIn": "Zoom: Přiblížit",
|
||||
"zoomOut": "Zoom: Oddálit",
|
||||
"pan": "Přesouvat",
|
||||
"reset": "Resetovat"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/da.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "da",
|
||||
"options": {
|
||||
"months": [
|
||||
"januar",
|
||||
"februar",
|
||||
"marts",
|
||||
"april",
|
||||
"maj",
|
||||
"juni",
|
||||
"juli",
|
||||
"august",
|
||||
"september",
|
||||
"oktober",
|
||||
"november",
|
||||
"december"
|
||||
],
|
||||
"shortMonths": [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"maj",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"okt",
|
||||
"nov",
|
||||
"dec"
|
||||
],
|
||||
"days": [
|
||||
"Søndag",
|
||||
"Mandag",
|
||||
"Tirsdag",
|
||||
"Onsdag",
|
||||
"Torsdag",
|
||||
"Fredag",
|
||||
"Lørdag"
|
||||
],
|
||||
"shortDays": ["Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Download SVG",
|
||||
"exportToPNG": "Download PNG",
|
||||
"exportToCSV": "Download CSV",
|
||||
"menu": "Menu",
|
||||
"selection": "Valg",
|
||||
"selectionZoom": "Zoom til valg",
|
||||
"zoomIn": "Zoom ind",
|
||||
"zoomOut": "Zoom ud",
|
||||
"pan": "Panorér",
|
||||
"reset": "Nulstil zoom"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/de.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "de",
|
||||
"options": {
|
||||
"months": [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mär",
|
||||
"Apr",
|
||||
"Mai",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Okt",
|
||||
"Nov",
|
||||
"Dez"
|
||||
],
|
||||
"days": [
|
||||
"Sonntag",
|
||||
"Montag",
|
||||
"Dienstag",
|
||||
"Mittwoch",
|
||||
"Donnerstag",
|
||||
"Freitag",
|
||||
"Samstag"
|
||||
],
|
||||
"shortDays": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "SVG speichern",
|
||||
"exportToPNG": "PNG speichern",
|
||||
"exportToCSV": "CSV speichern",
|
||||
"menu": "Menü",
|
||||
"selection": "Auswahl",
|
||||
"selectionZoom": "Auswahl vergrößern",
|
||||
"zoomIn": "Vergrößern",
|
||||
"zoomOut": "Verkleinern",
|
||||
"pan": "Verschieben",
|
||||
"reset": "Zoom zurücksetzen"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/el.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "el",
|
||||
"options": {
|
||||
"months": [
|
||||
"Ιανουάριος",
|
||||
"Φεβρουάριος",
|
||||
"Μάρτιος",
|
||||
"Απρίλιος",
|
||||
"Μάιος",
|
||||
"Ιούνιος",
|
||||
"Ιούλιος",
|
||||
"Αύγουστος",
|
||||
"Σεπτέμβριος",
|
||||
"Οκτώβριος",
|
||||
"Νοέμβριος",
|
||||
"Δεκέμβριος"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Ιαν",
|
||||
"Φευ",
|
||||
"Μαρ",
|
||||
"Απρ",
|
||||
"Μάι",
|
||||
"Ιουν",
|
||||
"Ιουλ",
|
||||
"Αυγ",
|
||||
"Σεπ",
|
||||
"Οκτ",
|
||||
"Νοε",
|
||||
"Δεκ"
|
||||
],
|
||||
"days": [
|
||||
"Κυριακή",
|
||||
"Δευτέρα",
|
||||
"Τρίτη",
|
||||
"Τετάρτη",
|
||||
"Πέμπτη",
|
||||
"Παρασκευή",
|
||||
"Σάββατο"
|
||||
],
|
||||
"shortDays": ["Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Λήψη SVG",
|
||||
"exportToPNG": "Λήψη PNG",
|
||||
"exportToCSV": "Λήψη CSV",
|
||||
"menu": "Menu",
|
||||
"selection": "Επιλογή",
|
||||
"selectionZoom": "Μεγένθυση βάση επιλογής",
|
||||
"zoomIn": "Μεγένθυνση",
|
||||
"zoomOut": "Σμίκρυνση",
|
||||
"pan": "Μετατόπιση",
|
||||
"reset": "Επαναφορά μεγένθυνσης"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/en.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "en",
|
||||
"options": {
|
||||
"months": [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec"
|
||||
],
|
||||
"days": [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
],
|
||||
"shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Download SVG",
|
||||
"exportToPNG": "Download PNG",
|
||||
"exportToCSV": "Download CSV",
|
||||
"menu": "Menu",
|
||||
"selection": "Selection",
|
||||
"selectionZoom": "Selection Zoom",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"pan": "Panning",
|
||||
"reset": "Reset Zoom"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/es.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "es",
|
||||
"options": {
|
||||
"months": [
|
||||
"Enero",
|
||||
"Febrero",
|
||||
"Marzo",
|
||||
"Abril",
|
||||
"Mayo",
|
||||
"Junio",
|
||||
"Julio",
|
||||
"Agosto",
|
||||
"Septiembre",
|
||||
"Octubre",
|
||||
"Noviembre",
|
||||
"Diciembre"
|
||||
],
|
||||
"shortMonths": [
|
||||
"Ene",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Abr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Ago",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dic"
|
||||
],
|
||||
"days": [
|
||||
"Domingo",
|
||||
"Lunes",
|
||||
"Martes",
|
||||
"Miércoles",
|
||||
"Jueves",
|
||||
"Viernes",
|
||||
"Sábado"
|
||||
],
|
||||
"shortDays": ["Dom", "Lun", "Mar", "Mie", "Jue", "Vie", "Sab"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Descargar SVG",
|
||||
"exportToPNG": "Descargar PNG",
|
||||
"exportToCSV": "Descargar CSV",
|
||||
"menu": "Menu",
|
||||
"selection": "Seleccionar",
|
||||
"selectionZoom": "Seleccionar Zoom",
|
||||
"zoomIn": "Aumentar",
|
||||
"zoomOut": "Disminuir",
|
||||
"pan": "Navegación",
|
||||
"reset": "Reiniciar Zoom"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ui/app/static/libs/apexcharts/locales/et.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "et",
|
||||
"options": {
|
||||
"months": [
|
||||
"jaanuar",
|
||||
"veebruar",
|
||||
"märts",
|
||||
"aprill",
|
||||
"mai",
|
||||
"juuni",
|
||||
"juuli",
|
||||
"august",
|
||||
"september",
|
||||
"oktoober",
|
||||
"november",
|
||||
"detsember"
|
||||
],
|
||||
"shortMonths": [
|
||||
"jaan",
|
||||
"veebr",
|
||||
"märts",
|
||||
"apr",
|
||||
"mai",
|
||||
"juuni",
|
||||
"juuli",
|
||||
"aug",
|
||||
"sept",
|
||||
"okt",
|
||||
"nov",
|
||||
"dets"
|
||||
],
|
||||
"days": [
|
||||
"pühapäev",
|
||||
"esmaspäev",
|
||||
"teisipäev",
|
||||
"kolmapäev",
|
||||
"neljapäev",
|
||||
"reede",
|
||||
"laupäev"
|
||||
],
|
||||
"shortDays": ["P", "E", "T", "K", "N", "R", "L"],
|
||||
"toolbar": {
|
||||
"exportToSVG": "Lae alla SVG",
|
||||
"exportToPNG": "Lae alla PNG",
|
||||
"exportToCSV": "Lae alla CSV",
|
||||
"menu": "Menüü",
|
||||
"selection": "Valik",
|
||||
"selectionZoom": "Valiku suum",
|
||||
"zoomIn": "Suurenda",
|
||||
"zoomOut": "Vähenda",
|
||||
"pan": "Panoraamimine",
|
||||
"reset": "Lähtesta suum"
|
||||
}
|
||||
}
|
||||
}
|
||||