Merge branch 'dev' into feature/steamcmd
@ -11,6 +11,7 @@ docker-compose.yml.example
|
||||
.gitlab/
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
lang_sort_log.txt
|
||||
|
||||
# root
|
||||
.editorconfig
|
||||
|
2
.gitignore
vendored
@ -18,6 +18,7 @@ env.bak/
|
||||
venv.bak/
|
||||
|
||||
.idea/
|
||||
/import/
|
||||
/imports/
|
||||
/servers/
|
||||
/app/frontend/static/assets/images/auth/custom/
|
||||
@ -35,3 +36,4 @@ default.json
|
||||
app/config/
|
||||
docker/*
|
||||
!docker/docker-compose.yml
|
||||
lang_sort_log.txt
|
||||
|
@ -44,7 +44,7 @@ black:
|
||||
# Code Climate/Quality Checking [https://pylint.pycqa.org/en/latest/]
|
||||
pylint:
|
||||
stage: lint
|
||||
image: registry.gitlab.com/pipeline-components/pylint:latest
|
||||
image: registry.gitlab.com/pipeline-components/pylint:0.21.1
|
||||
tags:
|
||||
- docker
|
||||
rules:
|
||||
@ -81,3 +81,26 @@ sonarcloud-check:
|
||||
- .sonar/cache
|
||||
script:
|
||||
- sonar-scanner
|
||||
|
||||
# Lang file checking
|
||||
lang-check:
|
||||
stage: lint
|
||||
image: alpine:latest
|
||||
tags:
|
||||
- docker
|
||||
rules:
|
||||
- if: "$CODE_QUALITY_DISABLED"
|
||||
when: never
|
||||
- if: "$CI_COMMIT_TAG || $CI_COMMIT_BRANCH"
|
||||
allow_failure: true
|
||||
before_script:
|
||||
- apk add --no-cache jq bash
|
||||
script:
|
||||
- chmod +x .gitlab/scripts/lang_sort.sh
|
||||
- bash .gitlab/scripts/lang_sort.sh ./app/translations/
|
||||
after_script:
|
||||
- if [ -f .gitlab/scripts/lang_sort_log.txt ]; then cat .gitlab/scripts/lang_sort_log.txt; fi
|
||||
artifacts:
|
||||
paths:
|
||||
- .gitlab/scripts/lang_sort_log.txt
|
||||
expire_in: 1 week
|
||||
|
95
.gitlab/scripts/lang_sort.sh
Normal file
@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ensure locale is set to C for predictable sorting
|
||||
export LC_ALL=C
|
||||
export LC_COLLATE=C
|
||||
|
||||
# Get the script's own path
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Directory containing the JSON files to sort
|
||||
DIR="$1"
|
||||
found_missing_keys=false
|
||||
|
||||
|
||||
##### Log Setup #####
|
||||
# Log file path
|
||||
LOGFILE="${SCRIPT_DIR}/lang_sort_log.txt"
|
||||
|
||||
# Redirect stdout and stderr to the logfile
|
||||
exec > "${LOGFILE}" 2>&1
|
||||
#####################
|
||||
|
||||
|
||||
##### Exit Gates #####
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq could not be found, please install jq first."
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check for directory argument
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 /path/to/translations"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if en_EN.json exists in the directory
|
||||
if [[ ! -f "${DIR}/en_EN.json" ]]; then
|
||||
echo "The file en_EN.json does not exist in ${DIR}.Ensure you have the right directory, Exiting."
|
||||
exit
|
||||
fi
|
||||
######################
|
||||
|
||||
|
||||
# Sort keys of the en_EN.json file with 4-space indentation and overwrite it
|
||||
jq -S --indent 4 '.' "${DIR}/en_EN.json" > "${DIR}/en_EN.json.tmp" && mv "${DIR}/en_EN.json.tmp" "${DIR}/en_EN.json"
|
||||
|
||||
# Function to recursively find all keys in a JSON object
|
||||
function get_keys {
|
||||
jq -r 'paths(scalars) | join("/")' "$1"
|
||||
}
|
||||
|
||||
# Get keys and subkeys from en_EN.json
|
||||
ref_keys=$(mktemp)
|
||||
get_keys "${DIR}/en_EN.json" | sort > "${ref_keys}"
|
||||
|
||||
# Iterate over each .json file in the directory
|
||||
for file in "${DIR}"/*.json; do
|
||||
# Check if file is a regular file and not en_EN.json, and does not contain "_incomplete" in its name
|
||||
if [[ -f "${file}" && "${file}" != "${DIR}/en_EN.json" && ! "${file}" =~ _incomplete ]]; then
|
||||
|
||||
# Get keys and subkeys from the current file
|
||||
current_keys=$(mktemp)
|
||||
get_keys "${file}" | sort > "${current_keys}"
|
||||
|
||||
# Display keys present in en_EN.json but not in the current file
|
||||
missing_keys=$(comm -23 "${ref_keys}" "${current_keys}")
|
||||
if [[ -n "${missing_keys}" ]]; then
|
||||
found_missing_keys=true
|
||||
echo -e "\nKeys/subkeys present in en_EN.json but missing in $(basename "${file}"): "
|
||||
echo "${missing_keys}"
|
||||
fi
|
||||
|
||||
# Sort keys of the JSON file and overwrite the original file
|
||||
jq -S --indent 4 '.' "${file}" > "${file}.tmp" && mv "${file}.tmp" "${file}"
|
||||
|
||||
# Remove the temporary file
|
||||
rm -f "${current_keys}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove the temporary file
|
||||
rm -f "${ref_keys}"
|
||||
|
||||
if ${found_missing_keys}; then
|
||||
echo -e "\n\nSorting complete!"
|
||||
echo "Comparison found missing keys, Please Review!"
|
||||
echo "-------------------------------------------------------------------"
|
||||
echo "If there are stale translations, you can exclude with '_incomplete'"
|
||||
echo " e.g. lol_EN_incomplete.json"
|
||||
echo "-------------------------------------------------------------------"
|
||||
exit 1
|
||||
else
|
||||
echo -e "\n\nComparison and Sorting complete!"
|
||||
fi
|
82
CHANGELOG.md
@ -1,20 +1,94 @@
|
||||
# Changelog
|
||||
## --- [4.2.0] - 2023/TBD
|
||||
## --- [4.2.2] - 2023/TBD
|
||||
### New features
|
||||
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
|
||||
- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
|
||||
### Refactor
|
||||
- Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670))
|
||||
- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
|
||||
- Force random password on first run. Stop using common default password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/672) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/673))
|
||||
### Bug fixes
|
||||
- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664))
|
||||
- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680))
|
||||
- Fix bug where su cannot edit general user password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/676))
|
||||
- Fix bug where no file error on import root dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/677))
|
||||
- Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671))
|
||||
- Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674))
|
||||
- Fix bug where you cannot select "do not monitor mounts" from `config.json` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/678))
|
||||
- Fix support log 'x' button still downloading logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/679))
|
||||
- Fix bug where servers are created without bu dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/682))
|
||||
### Tweaks
|
||||
- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666))
|
||||
- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
|
||||
- Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
|
||||
- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
|
||||
- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
|
||||
- Give better feedback when backup delete fails ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/681))
|
||||
### Lang
|
||||
- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675))
|
||||
<br><br>
|
||||
|
||||
## --- [4.2.1] - 2023/11/01
|
||||
### Bug fixes
|
||||
- Fix logic issue with `get_files` API permissions check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/654))
|
||||
- Fix notifications not showing up/being reset #298 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/660))
|
||||
- Fix users not being able to be deleted since the prompt fails to display ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/661))
|
||||
- Fix duplicate function naming on dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/662))
|
||||
### Tweaks
|
||||
- Auto refresh Crafty Announcements on 30m interval ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/653))
|
||||
- Improve Crafty toggle buttons and Webhooks page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
|
||||
### Lang
|
||||
- Update `zh_CN` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/652))
|
||||
- Update `es_ES` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/655))
|
||||
- Clean up wording in `pl_PL` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
|
||||
- Add `de_DE`, `es_ES` `fr_FR`, `lol_EN`, `lv_LV`, `nl_BE` `pl_PL` & `zh_CN` translations for !656 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
|
||||
### Docs
|
||||
- [(New) Server Webhook Documentation](https://docs.craftycontrol.com/pages/user-guide/webhooks/)
|
||||
- [(Edit) Image Context in Windows Service - Install steps, with slight wording improvement](https://docs.craftycontrol.com/pages/getting-started/installation/windows/#install-steps)
|
||||
<br><br>
|
||||
|
||||
## --- [4.2.0] - 2023/10/18
|
||||
### New features
|
||||
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/626) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/632))
|
||||
- Add initial Webhook Notification (Discord, Mattermost, Slack, Teams) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/594))
|
||||
- Implementation of OpenMetrics endpoints, for use with services such as Prometheus ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/624))
|
||||
### Bug fixes
|
||||
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
|
||||
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
|
||||
- Correctly handle if a server returns a string instead of json data on socket ping ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/614))
|
||||
- Bump tornado to resolve #269 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/623))
|
||||
- Bump crypto to resolve #267 & #268 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/622))
|
||||
- Fix select installs failing to start, returning missing python package `packaging` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/629))
|
||||
- Fix public status page not updating #255 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/615))
|
||||
- Fix service worker vulrn and CQ raised by SonarQ ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/631))
|
||||
- Fix Backup Restore/Schedules, Backup button function on `remote-comms2` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/634))
|
||||
- Add a wait to the call for the directory so we can make sure the wait dialogue has time to show up first ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/637))
|
||||
- Fix bug where a reaction loop could be created, but would be cut short by an error when the loop occurred ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/636))
|
||||
- Use controller on update user call ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/640))
|
||||
- Move `imports` to `import/upload` in bind mount to better serve users on unraid with limited vdisk storage ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/642))
|
||||
- Fix bug where everytime a page was loaded user settings would be reset #286 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/643))
|
||||
- Fix tooltip info icon on server config page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/647))
|
||||
- Fix quick disable toggle on schedules list ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/649))
|
||||
### Refactor
|
||||
- Consolidate remaining frontend functions into API V2, and remove ajax internal API ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/585))
|
||||
- Replace bleach with nh3 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/628))
|
||||
- Add API route for historical server stats ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/615))
|
||||
- Add API route for host stats ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/615))
|
||||
### Tweaks
|
||||
- Polish/Enhance display for InApp Documentation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/613))
|
||||
- Add get_users command to Crafty's console ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/620))
|
||||
- Add `get_users` command to Crafty's console ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/620))
|
||||
- Make files hover cursor pointer ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/627))
|
||||
- Use `Jar` class naming for jar refresh to make room for steamCMD naming in the future ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/630))
|
||||
- Improve ui visibility of Build Wizard selection tabs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/633))
|
||||
- Add additional logging for server bootstrap & moves unnecessary logging to `debug` for improved log clarity ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/635))
|
||||
- Bump orjson to `3.9.7` for python `3.12` support ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/638))
|
||||
- Bump all Crafty required python dependancies, maintaining minimum `3.9` support ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/639)) Revert peewee bump ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/651))
|
||||
- Better optimize and refactor docker launcher sh ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/642))
|
||||
- Improve pop-up notifications with Toasts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/641))
|
||||
- Move username and password settings to buttons on panel config ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/643))
|
||||
- Remove external references from front end deps ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/648))
|
||||
### Lang
|
||||
TBD
|
||||
- `fr_FR` Translation Updated to latest en_EN ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/646))
|
||||
- `de_DE`, `fr_FR`, `lol_EN`, `lv_LV`, `nl_BE`, `pl_PL` Translations Updated to latest `en_EN` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/645))
|
||||
<br><br>
|
||||
|
||||
## --- [4.1.3] - 2023/07/18
|
||||
|
@ -1,5 +1,5 @@
|
||||
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
|
||||
# Crafty Controller 4.2.0
|
||||
# Crafty Controller 4.2.2
|
||||
> Python based Control Panel for your Minecraft Server
|
||||
|
||||
## What is Crafty Controller?
|
||||
|
@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import queue
|
||||
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from prometheus_client import CollectorRegistry, Gauge
|
||||
|
||||
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||
from app.classes.models.servers import HelperServers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -11,6 +13,8 @@ class ManagementController:
|
||||
def __init__(self, management_helper):
|
||||
self.management_helper = management_helper
|
||||
self.command_queue = queue.Queue()
|
||||
self.host_registry = CollectorRegistry()
|
||||
self.init_host_registries()
|
||||
|
||||
# **********************************************************************************
|
||||
# Config Methods
|
||||
@ -54,6 +58,19 @@ class ManagementController:
|
||||
def add_crafty_row():
|
||||
HelpersManagement.create_crafty_row()
|
||||
|
||||
def init_host_registries(self):
|
||||
# REGISTRY Entries for Server Stats functions
|
||||
self.cpu_usage = Gauge(
|
||||
name="CPU_Usage",
|
||||
documentation="The CPU usage of the server",
|
||||
registry=self.host_registry,
|
||||
)
|
||||
self.mem_usage_percent = Gauge(
|
||||
name="Mem_Usage",
|
||||
documentation="The Memory usage of the server",
|
||||
registry=self.host_registry,
|
||||
)
|
||||
|
||||
# **********************************************************************************
|
||||
# Commands Methods
|
||||
# **********************************************************************************
|
||||
@ -206,3 +223,30 @@ class ManagementController:
|
||||
@staticmethod
|
||||
def set_master_server_dir(server_dir):
|
||||
HelpersManagement.set_master_server_dir(server_dir)
|
||||
|
||||
# **********************************************************************************
|
||||
# Webhooks Methods
|
||||
# **********************************************************************************
|
||||
@staticmethod
|
||||
def create_webhook(data):
|
||||
return HelpersWebhooks.create_webhook(data)
|
||||
|
||||
@staticmethod
|
||||
def modify_webhook(webhook_id, data):
|
||||
HelpersWebhooks.modify_webhook(webhook_id, data)
|
||||
|
||||
@staticmethod
|
||||
def get_webhook_by_id(webhook_id):
|
||||
return HelpersWebhooks.get_webhook_by_id(webhook_id)
|
||||
|
||||
@staticmethod
|
||||
def get_webhooks_by_server(server_id, model=False):
|
||||
return HelpersWebhooks.get_webhooks_by_server(server_id, model)
|
||||
|
||||
@staticmethod
|
||||
def delete_webhook(webhook_id):
|
||||
HelpersWebhooks.delete_webhook(webhook_id)
|
||||
|
||||
@staticmethod
|
||||
def delete_webhook_by_server(server_id):
|
||||
HelpersWebhooks.delete_webhooks_by_server(server_id)
|
||||
|
@ -22,6 +22,7 @@ from app.classes.models.server_permissions import (
|
||||
PermissionsServers,
|
||||
EnumPermissionsServer,
|
||||
)
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -36,6 +37,8 @@ class ServersController(metaclass=Singleton):
|
||||
self.management_helper = management_helper
|
||||
self.servers_list = []
|
||||
self.stats = Stats(self.helper, self)
|
||||
self.web_sock = WebSocketManager()
|
||||
self.server_subpage = {}
|
||||
|
||||
# **********************************************************************************
|
||||
# Generic Servers Methods
|
||||
@ -107,9 +110,9 @@ class ServersController(metaclass=Singleton):
|
||||
|
||||
return ret
|
||||
|
||||
def get_history_stats(self, server_id, days):
|
||||
def get_history_stats(self, server_id, hours):
|
||||
srv = ServersController().get_server_instance_by_id(server_id)
|
||||
return srv.stats_helper.get_history_stats(server_id, days)
|
||||
return srv.stats_helper.get_history_stats(server_id, hours)
|
||||
|
||||
@staticmethod
|
||||
def update_unloaded_server(server_obj):
|
||||
@ -171,8 +174,15 @@ class ServersController(metaclass=Singleton):
|
||||
def init_all_servers(self):
|
||||
servers = self.get_all_defined_servers()
|
||||
self.failed_servers = []
|
||||
|
||||
for server in servers:
|
||||
self.web_sock.broadcast_to_admins(
|
||||
"update",
|
||||
{"section": "server", "server": server["server_name"]},
|
||||
)
|
||||
self.web_sock.broadcast_to_non_admins(
|
||||
"update",
|
||||
{"section": "init"},
|
||||
)
|
||||
server_id = server.get("server_id")
|
||||
|
||||
# if we have already initialized this server, let's skip it.
|
||||
|
@ -45,8 +45,7 @@ class UsersController:
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 6,
|
||||
"minLength": 8,
|
||||
"examples": ["crafty"],
|
||||
"title": "Password",
|
||||
},
|
||||
@ -214,14 +213,14 @@ class UsersController:
|
||||
limit_server_creation = 0
|
||||
limit_user_creation = 0
|
||||
limit_role_creation = 0
|
||||
|
||||
PermissionsCrafty.add_or_update_user(
|
||||
user_id,
|
||||
permissions_mask,
|
||||
limit_server_creation,
|
||||
limit_user_creation,
|
||||
limit_role_creation,
|
||||
)
|
||||
if user_crafty_data:
|
||||
PermissionsCrafty.add_or_update_user(
|
||||
user_id,
|
||||
permissions_mask,
|
||||
limit_server_creation,
|
||||
limit_user_creation,
|
||||
limit_role_creation,
|
||||
)
|
||||
|
||||
self.users_helper.delete_user_roles(user_id, removed_roles)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import requests
|
||||
|
||||
from app.classes.controllers.servers_controller import ServersController
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -179,9 +180,7 @@ class ServerJars:
|
||||
try:
|
||||
ServersController.set_import(server_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
user, "send_start_reload", {}
|
||||
)
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
|
||||
break
|
||||
except Exception as ex:
|
||||
@ -206,11 +205,9 @@ class ServerJars:
|
||||
server_users = PermissionsServers.get_server_user_list(server_id)
|
||||
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user, "notification", "Executable download finished"
|
||||
)
|
||||
time.sleep(3)
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
user, "send_start_reload", {}
|
||||
)
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
return success
|
||||
|
@ -226,7 +226,7 @@ class Stats:
|
||||
def get_server_players(self, server_id):
|
||||
server = HelperServers.get_server_data_by_id(server_id)
|
||||
|
||||
logger.info(f"Getting players for server {server}")
|
||||
logger.debug(f"Getting players for server {server['server_name']}")
|
||||
|
||||
internal_ip = server["server_ip"]
|
||||
server_port = server["server_port"]
|
||||
|
@ -17,6 +17,7 @@ from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.servers import Servers
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -78,11 +79,15 @@ class HostStats(BaseModel):
|
||||
# **********************************************************************************
|
||||
class Webhooks(BaseModel):
|
||||
id = AutoField()
|
||||
name = CharField(max_length=64, unique=True, index=True)
|
||||
method = CharField(default="POST")
|
||||
url = CharField(unique=True)
|
||||
event = CharField(default="")
|
||||
send_data = BooleanField(default=True)
|
||||
server_id = IntegerField(null=True)
|
||||
name = CharField(default="Custom Webhook", max_length=64)
|
||||
url = CharField(default="")
|
||||
webhook_type = CharField(default="Custom")
|
||||
bot_name = CharField(default="Crafty Controller")
|
||||
trigger = CharField(default="server_start,server_stop")
|
||||
body = CharField(default="")
|
||||
color = CharField(default="#005cd1")
|
||||
enabled = BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
table_name = "webhooks"
|
||||
@ -158,9 +163,7 @@ class HelpersManagement:
|
||||
server_users = PermissionsServers.get_server_user_list(server_id)
|
||||
for user in server_users:
|
||||
try:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
user, "notification", audit_msg
|
||||
)
|
||||
WebSocketManager().broadcast_user(user, "notification", audit_msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Error broadcasting to user {user} - {e}")
|
||||
|
||||
@ -502,3 +505,82 @@ class HelpersManagement:
|
||||
f"Not removing {dir_to_del} from excluded directories - "
|
||||
f"not in the excluded directory list for server ID {server_id}"
|
||||
)
|
||||
|
||||
|
||||
# **********************************************************************************
|
||||
# Webhooks Class
|
||||
# **********************************************************************************
|
||||
class HelpersWebhooks:
|
||||
def __init__(self, database):
|
||||
self.database = database
|
||||
|
||||
@staticmethod
|
||||
def create_webhook(create_data) -> int:
|
||||
"""Create a webhook in the database
|
||||
|
||||
Args:
|
||||
server_id: ID of a server this webhook will be married to
|
||||
name: The name of the webhook
|
||||
url: URL to the webhook
|
||||
webhook_type: The provider this webhook will be sent to
|
||||
bot name: The name that will appear when the webhook is sent
|
||||
triggers: Server actions that will trigger this webhook
|
||||
body: The message body of the webhook
|
||||
enabled: Should Crafty trigger the webhook
|
||||
|
||||
Returns:
|
||||
int: The new webhooks's id
|
||||
|
||||
Raises:
|
||||
PeeweeException: If the webhook already exists
|
||||
"""
|
||||
return Webhooks.insert(
|
||||
{
|
||||
Webhooks.server_id: create_data["server_id"],
|
||||
Webhooks.name: create_data["name"],
|
||||
Webhooks.webhook_type: create_data["webhook_type"],
|
||||
Webhooks.url: create_data["url"],
|
||||
Webhooks.bot_name: create_data["bot_name"],
|
||||
Webhooks.body: create_data["body"],
|
||||
Webhooks.color: create_data["color"],
|
||||
Webhooks.trigger: create_data["trigger"],
|
||||
Webhooks.enabled: create_data["enabled"],
|
||||
}
|
||||
).execute()
|
||||
|
||||
@staticmethod
|
||||
def modify_webhook(webhook_id, updata):
|
||||
Webhooks.update(updata).where(Webhooks.id == webhook_id).execute()
|
||||
|
||||
@staticmethod
|
||||
def get_webhook_by_id(webhook_id):
|
||||
return model_to_dict(Webhooks.get(Webhooks.id == webhook_id))
|
||||
|
||||
@staticmethod
|
||||
def get_webhooks_by_server(server_id, model):
|
||||
if not model:
|
||||
data = {}
|
||||
for webhook in (
|
||||
Webhooks.select().where(Webhooks.server_id == server_id).execute()
|
||||
):
|
||||
data[str(webhook.id)] = {
|
||||
"webhook_type": webhook.webhook_type,
|
||||
"name": webhook.name,
|
||||
"url": webhook.url,
|
||||
"bot_name": webhook.bot_name,
|
||||
"trigger": webhook.trigger,
|
||||
"body": webhook.body,
|
||||
"color": webhook.color,
|
||||
"enabled": webhook.enabled,
|
||||
}
|
||||
else:
|
||||
data = Webhooks.select().where(Webhooks.server_id == server_id).execute()
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def delete_webhook(webhook_id):
|
||||
Webhooks.delete().where(Webhooks.id == webhook_id).execute()
|
||||
|
||||
@staticmethod
|
||||
def delete_webhooks_by_server(server_id):
|
||||
Webhooks.delete().where(Webhooks.server_id == server_id).execute()
|
||||
|
@ -8,6 +8,7 @@ from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.shared.migration import MigrationManager
|
||||
|
||||
|
||||
try:
|
||||
from peewee import (
|
||||
SqliteDatabase,
|
||||
@ -50,6 +51,7 @@ class ServerStats(Model):
|
||||
max = IntegerField(default=0)
|
||||
players = CharField(default="")
|
||||
desc = CharField(default="Unable to Connect")
|
||||
icon = CharField(default="")
|
||||
version = CharField(default="")
|
||||
updating = BooleanField(default=False)
|
||||
waiting_start = BooleanField(default=False)
|
||||
@ -141,16 +143,20 @@ class HelperServerStats:
|
||||
self.database.close()
|
||||
return server_data
|
||||
|
||||
def get_history_stats(self, server_id, num_days):
|
||||
def get_history_stats(self, server_id, num_hours):
|
||||
self.database.connect(reuse_if_open=True)
|
||||
max_age = datetime.datetime.now() - timedelta(days=num_days)
|
||||
server_stats = (
|
||||
max_age = datetime.datetime.now() - timedelta(hours=num_hours)
|
||||
query_stats = (
|
||||
ServerStats.select()
|
||||
.where(ServerStats.created > max_age)
|
||||
.where(ServerStats.server_id == server_id)
|
||||
# .order_by(ServerStats.created.desc())
|
||||
.execute(self.database)
|
||||
)
|
||||
self.database.connect(reuse_if_open=True)
|
||||
server_stats = []
|
||||
for stat in query_stats:
|
||||
server_stats.append(DatabaseShortcuts.get_data_obj(stat))
|
||||
self.database.close()
|
||||
return server_stats
|
||||
|
||||
def insert_server_stats(self, server_stats):
|
||||
@ -179,6 +185,7 @@ class HelperServerStats:
|
||||
ServerStats.max: server_stats.get("max", False),
|
||||
ServerStats.players: server_stats.get("players", False),
|
||||
ServerStats.desc: server_stats.get("desc", False),
|
||||
ServerStats.icon: server_stats.get("icon", None),
|
||||
ServerStats.version: server_stats.get("version", False),
|
||||
}
|
||||
).execute(self.database)
|
||||
|
@ -11,6 +11,7 @@ from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.tasks import TasksManager
|
||||
from app.classes.shared.migration import MigrationManager
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -118,7 +119,7 @@ class MainPrompt(cmd.Cmd):
|
||||
Console.info(
|
||||
"Stopping all server daemons / threads - This may take a few seconds"
|
||||
)
|
||||
self.helper.websocket_helper.disconnect_all()
|
||||
WebSocketManager().disconnect_all()
|
||||
Console.info("Waiting for main thread to stop")
|
||||
while True:
|
||||
if self.tasks_manager.get_main_thread_run_status():
|
||||
|
@ -8,6 +8,7 @@ from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -149,7 +150,7 @@ class FileHelpers:
|
||||
"percent": 0,
|
||||
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
||||
}
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
@ -194,7 +195,7 @@ class FileHelpers:
|
||||
"percent": percent,
|
||||
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
||||
}
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
@ -215,7 +216,7 @@ class FileHelpers:
|
||||
"percent": 0,
|
||||
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
||||
}
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
@ -274,7 +275,7 @@ class FileHelpers:
|
||||
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
||||
}
|
||||
# send status results to page.
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
|
@ -29,7 +29,6 @@ from app.classes.shared.null_writer import NullWriter
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.installer import installer
|
||||
from app.classes.shared.translation import Translation
|
||||
from app.classes.web.websocket_helper import WebSocketHelper
|
||||
|
||||
with redirect_stderr(NullWriter()):
|
||||
import psutil
|
||||
@ -79,10 +78,10 @@ class Helpers:
|
||||
self.passhasher = PasswordHasher()
|
||||
self.exiting = False
|
||||
|
||||
self.websocket_helper = WebSocketHelper(self)
|
||||
self.translation = Translation(self)
|
||||
self.update_available = False
|
||||
self.ignored_names = ["crafty_managed.txt", "db_stats"]
|
||||
self.crafty_starting = False
|
||||
|
||||
@staticmethod
|
||||
def auto_installer_fix(ex):
|
||||
@ -364,6 +363,42 @@ class Helpers:
|
||||
|
||||
return result_of_check == 0
|
||||
|
||||
def create_pass(self):
|
||||
# Maximum length of password needed
|
||||
max_len = 64
|
||||
|
||||
# Declare string of the character that we need in our password
|
||||
digits = string.digits
|
||||
locase = string.ascii_lowercase
|
||||
upcase = string.ascii_uppercase
|
||||
symbols = "!@#$%^&*" # Reducing to avoid issues with ([]{}<>,'`) etc
|
||||
|
||||
# Combine all the character strings above to form one string
|
||||
combo = digits + upcase + locase + symbols
|
||||
|
||||
# Randomly select at least one character from each character set above
|
||||
rand_digit = secrets.choice(digits)
|
||||
rand_upper = secrets.choice(upcase)
|
||||
rand_lower = secrets.choice(locase)
|
||||
rand_symbol = secrets.choice(symbols)
|
||||
|
||||
# Combine the character randomly selected above
|
||||
temp_pass = rand_digit + rand_upper + rand_lower + rand_symbol
|
||||
|
||||
# Fill the rest of the password length by selecting randomly char list
|
||||
for _ in range(max_len - 4):
|
||||
temp_pass += secrets.choice(combo)
|
||||
|
||||
# Shuffle the temporary password to prevent predictable patterns
|
||||
temp_pass_list = list(temp_pass)
|
||||
secrets.SystemRandom().shuffle(temp_pass_list)
|
||||
|
||||
# Form the password by concatenating the characters
|
||||
password = "".join(temp_pass_list)
|
||||
|
||||
# Return completed password
|
||||
return password
|
||||
|
||||
@staticmethod
|
||||
def cmdparse(cmd_in):
|
||||
# Parse a string into arguments
|
||||
@ -581,16 +616,19 @@ class Helpers:
|
||||
return version_data
|
||||
|
||||
def get_announcements(self):
|
||||
data = []
|
||||
try:
|
||||
data = []
|
||||
response = requests.get("https://craftycontrol.com/notify", timeout=2)
|
||||
data = json.loads(response.content)
|
||||
if self.update_available:
|
||||
data.append(self.update_available)
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch notifications with error: {e}")
|
||||
|
||||
if self.update_available:
|
||||
data.append(self.update_available)
|
||||
return data
|
||||
if self.update_available:
|
||||
data = [self.update_available]
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_version_string(self):
|
||||
version_data = self.get_version()
|
||||
|
@ -9,9 +9,11 @@ from app.classes.controllers.server_perms_controller import PermissionsServers
|
||||
from app.classes.controllers.servers_controller import ServersController
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
from app.classes.steamcmd.serverapps import SteamApps
|
||||
from app.classes.steamcmd.steamcmd import SteamCMD
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -68,7 +70,7 @@ class ImportHelpers:
|
||||
ServersController.finish_import(new_id)
|
||||
server_users = PermissionsServers.get_server_user_list(new_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
|
||||
def import_java_zip_server(self, temp_dir, new_server_dir, port, new_id):
|
||||
import_thread = threading.Thread(
|
||||
@ -112,7 +114,7 @@ class ImportHelpers:
|
||||
server_users = PermissionsServers.get_server_user_list(new_id)
|
||||
ServersController.finish_import(new_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
# deletes temp dir
|
||||
FileHelpers.del_dirs(temp_dir)
|
||||
|
||||
@ -166,7 +168,7 @@ class ImportHelpers:
|
||||
ServersController.finish_import(new_id)
|
||||
server_users = PermissionsServers.get_server_user_list(new_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
|
||||
def import_bedrock_zip_server(
|
||||
self, temp_dir, new_server_dir, full_jar_path, port, new_id
|
||||
@ -213,7 +215,7 @@ class ImportHelpers:
|
||||
ServersController.finish_import(new_id)
|
||||
server_users = PermissionsServers.get_server_user_list(new_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
if os.name != "nt":
|
||||
if Helpers.check_file_exists(full_jar_path):
|
||||
os.chmod(full_jar_path, 0o2760)
|
||||
@ -285,4 +287,4 @@ class ImportHelpers:
|
||||
ServersController.finish_import(new_id)
|
||||
server_users = PermissionsServers.get_server_user_list(new_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
|
@ -8,11 +8,11 @@ import time
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from zoneinfo import ZoneInfoNotFoundError
|
||||
from peewee import DoesNotExist
|
||||
|
||||
# TZLocal is set as a hidden import on win pipeline
|
||||
from tzlocal import get_localzone
|
||||
from tzlocal.utils import ZoneInfoNotFoundError
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
@ -33,8 +33,10 @@ from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.import_helper import ImportHelpers
|
||||
from app.classes.minecraft.serverjars import ServerJars
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
from app.classes.steamcmd.serverapps import SteamApps
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -79,6 +81,37 @@ class Controller:
|
||||
self.first_login = False
|
||||
self.cached_login = self.management.get_login_image()
|
||||
self.support_scheduler.start()
|
||||
try:
|
||||
with open(
|
||||
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
self.auth_tracker = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
self.auth_tracker = {}
|
||||
|
||||
def log_attempt(self, remote_ip, username):
|
||||
remote = self.auth_tracker.get(str(remote_ip), None)
|
||||
if remote:
|
||||
remote["names"].append(username)
|
||||
remote["attempts"] += 1
|
||||
remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
|
||||
self.auth_tracker[str(remote_ip)] = remote
|
||||
else:
|
||||
self.auth_tracker[str(remote_ip)] = {
|
||||
"names": [username],
|
||||
"attempts": 1,
|
||||
"times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")],
|
||||
}
|
||||
|
||||
def write_auth_tracker(self):
|
||||
with open(
|
||||
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
json.dump(self.auth_tracker, f, indent=4)
|
||||
|
||||
@staticmethod
|
||||
def check_system_user():
|
||||
@ -115,7 +148,7 @@ class Controller:
|
||||
self.del_support_file(exec_user["support_logs"])
|
||||
# pausing so on screen notifications can run for user
|
||||
time.sleep(7)
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
exec_user["user_id"], "notification", "Preparing your support logs"
|
||||
)
|
||||
self.helper.ensure_dir_exists(
|
||||
@ -211,17 +244,15 @@ class Controller:
|
||||
) as f:
|
||||
f.write(sys_info_string)
|
||||
FileHelpers.make_compressed_archive(temp_zip_storage, temp_dir, sys_info_string)
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_user(
|
||||
exec_user["user_id"],
|
||||
"support_status_update",
|
||||
Helpers.calc_percent(temp_dir, temp_zip_storage + ".zip"),
|
||||
)
|
||||
|
||||
temp_zip_storage += ".zip"
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
exec_user["user_id"], "send_logs_bootbox", {}
|
||||
)
|
||||
WebSocketManager().broadcast_user(exec_user["user_id"], "send_logs_bootbox", {})
|
||||
|
||||
self.users.set_support_path(exec_user["user_id"], temp_zip_storage)
|
||||
|
||||
@ -254,8 +285,8 @@ class Controller:
|
||||
results = Helpers.calc_percent(source_path, dest_path)
|
||||
self.log_stats = results
|
||||
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_user(
|
||||
exec_user["user_id"], "support_status_update", results
|
||||
)
|
||||
|
||||
@ -511,6 +542,10 @@ class Controller:
|
||||
server_host=monitoring_host,
|
||||
server_type=monitoring_type,
|
||||
)
|
||||
self.management.set_backup_config(
|
||||
new_server_id,
|
||||
backup_path,
|
||||
)
|
||||
if data["create_type"] == "minecraft_java":
|
||||
if root_create_data["create_type"] == "download_jar":
|
||||
# modded update urls from server jars will only update the installer
|
||||
@ -616,6 +651,66 @@ class Controller:
|
||||
return False
|
||||
return True
|
||||
|
||||
def restore_java_zip_server(
|
||||
self,
|
||||
server_name: str,
|
||||
zip_path: str,
|
||||
server_jar: str,
|
||||
min_mem: int,
|
||||
max_mem: int,
|
||||
port: int,
|
||||
user_id: int,
|
||||
):
|
||||
server_id = Helpers.create_uuid()
|
||||
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
|
||||
backup_path = os.path.join(self.helper.backup_path, server_id)
|
||||
if Helpers.is_os_windows():
|
||||
new_server_dir = Helpers.wtol_path(new_server_dir)
|
||||
backup_path = Helpers.wtol_path(backup_path)
|
||||
new_server_dir.replace(" ", "^ ")
|
||||
backup_path.replace(" ", "^ ")
|
||||
|
||||
temp_dir = Helpers.get_os_understandable_path(zip_path)
|
||||
Helpers.ensure_dir_exists(new_server_dir)
|
||||
Helpers.ensure_dir_exists(backup_path)
|
||||
|
||||
full_jar_path = os.path.join(new_server_dir, server_jar)
|
||||
|
||||
if Helpers.is_os_windows():
|
||||
server_command = (
|
||||
f"java -Xms{Helpers.float_to_string(min_mem)}M "
|
||||
f"-Xmx{Helpers.float_to_string(max_mem)}M "
|
||||
f'-jar "{full_jar_path}" nogui'
|
||||
)
|
||||
else:
|
||||
server_command = (
|
||||
f"java -Xms{Helpers.float_to_string(min_mem)}M "
|
||||
f"-Xmx{Helpers.float_to_string(max_mem)}M "
|
||||
f"-jar {full_jar_path} nogui"
|
||||
)
|
||||
logger.debug("command: " + server_command)
|
||||
server_log_file = "./logs/latest.log"
|
||||
server_stop = "stop"
|
||||
|
||||
new_id = self.register_server(
|
||||
server_name,
|
||||
server_id,
|
||||
new_server_dir,
|
||||
backup_path,
|
||||
server_command,
|
||||
server_jar,
|
||||
server_log_file,
|
||||
server_stop,
|
||||
port,
|
||||
user_id,
|
||||
server_type="minecraft-java",
|
||||
)
|
||||
ServersController.set_import(new_id)
|
||||
self.import_helper.import_java_zip_server(
|
||||
temp_dir, new_server_dir, port, new_id
|
||||
)
|
||||
return new_id
|
||||
|
||||
# **********************************************************************************
|
||||
# BEDROCK IMPORTS
|
||||
# **********************************************************************************
|
||||
@ -713,7 +808,7 @@ class Controller:
|
||||
self.import_helper.download_bedrock_server(new_server_dir, new_id)
|
||||
return new_id
|
||||
|
||||
def import_bedrock_zip_server(
|
||||
def restore_bedrock_zip_server(
|
||||
self,
|
||||
server_name: str,
|
||||
zip_path: str,
|
||||
@ -860,6 +955,7 @@ class Controller:
|
||||
|
||||
srv_obj = server["server_obj"]
|
||||
srv_obj.server_scheduler.shutdown()
|
||||
srv_obj.dir_scheduler.shutdown()
|
||||
running = srv_obj.check_running()
|
||||
|
||||
if running:
|
||||
@ -933,7 +1029,7 @@ class Controller:
|
||||
def t_update_master_server_dir(self, new_server_path, user_id):
|
||||
new_server_path = self.helper.wtol_path(new_server_path)
|
||||
new_server_path = os.path.join(new_server_path, "servers")
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/panel_config", "move_status", "Checking dir"
|
||||
)
|
||||
current_master = self.helper.wtol_path(
|
||||
@ -943,7 +1039,7 @@ class Controller:
|
||||
logger.info(
|
||||
"Admin tried to change server dir to current server dir. Canceling..."
|
||||
)
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/panel_config",
|
||||
"move_status",
|
||||
"done",
|
||||
@ -954,18 +1050,18 @@ class Controller:
|
||||
"Admin tried to change server dir to be inside a sub directory of the"
|
||||
" current server dir. This will result in a copy loop."
|
||||
)
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/panel_config",
|
||||
"move_status",
|
||||
"done",
|
||||
)
|
||||
return
|
||||
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/panel_config", "move_status", "Checking permissions"
|
||||
)
|
||||
if not self.helper.ensure_dir_exists(new_server_path):
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -989,7 +1085,7 @@ class Controller:
|
||||
new_server_path, server.get("server_uuid")
|
||||
)
|
||||
if os.path.isdir(server_path):
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/panel_config",
|
||||
"move_status",
|
||||
f"Moving {server.get('server_name')}",
|
||||
@ -1030,7 +1126,7 @@ class Controller:
|
||||
self.servers.update_unloaded_server(server_obj)
|
||||
self.servers.init_all_servers()
|
||||
self.helper.dir_migration = False
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/panel_config",
|
||||
"move_status",
|
||||
"done",
|
||||
|
@ -14,13 +14,17 @@ class DatabaseBuilder:
|
||||
self.management_helper = management_helper
|
||||
self.users_helper = users_helper
|
||||
|
||||
def default_settings(self):
|
||||
def default_settings(self, password="crafty"):
|
||||
logger.info("Fresh Install Detected - Creating Default Settings")
|
||||
Console.info("Fresh Install Detected - Creating Default Settings")
|
||||
default_data = self.helper.find_default_password()
|
||||
|
||||
if password not in default_data:
|
||||
Console.help(
|
||||
"No default password found. Using password created "
|
||||
"by Crafty. Find it in app/config/default-creds.txt"
|
||||
)
|
||||
username = default_data.get("username", "admin")
|
||||
password = default_data.get("password", "crafty")
|
||||
password = default_data.get("password", password)
|
||||
|
||||
self.users_helper.add_user(
|
||||
username=username,
|
||||
|
@ -16,22 +16,27 @@ import json
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# TZLocal is set as a hidden import on win pipeline
|
||||
from zoneinfo import ZoneInfoNotFoundError
|
||||
from tzlocal import get_localzone
|
||||
from tzlocal.utils import ZoneInfoNotFoundError
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.jobstores.base import JobLookupError
|
||||
from apscheduler.jobstores.base import JobLookupError, ConflictingIdError
|
||||
|
||||
# OpenMetrics/Prometheus Imports
|
||||
from prometheus_client import CollectorRegistry, Gauge, Info
|
||||
|
||||
from app.classes.minecraft.stats import Stats
|
||||
from app.classes.minecraft.ping import ping, ping_raknet
|
||||
from app.classes.models.servers import HelperServers, Servers
|
||||
from app.classes.models.server_stats import HelperServerStats
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.null_writer import NullWriter
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
with redirect_stderr(NullWriter()):
|
||||
import psutil
|
||||
@ -40,6 +45,45 @@ with redirect_stderr(NullWriter()):
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def callback(called_func):
|
||||
# Usage of @callback on method
|
||||
# definition to run a webhook check
|
||||
# on method completion
|
||||
def wrapper(*args, **kwargs):
|
||||
res = None
|
||||
logger.debug("Checking for callbacks")
|
||||
try:
|
||||
res = called_func(*args, **kwargs)
|
||||
finally:
|
||||
events = WebhookFactory.get_monitored_events()
|
||||
if called_func.__name__ in events:
|
||||
server_webhooks = HelpersWebhooks.get_webhooks_by_server(
|
||||
args[0].server_id, True
|
||||
)
|
||||
for swebhook in server_webhooks:
|
||||
if called_func.__name__ in str(swebhook.trigger).split(","):
|
||||
logger.info(
|
||||
f"Found callback for event {called_func.__name__}"
|
||||
f" for server {args[0].server_id}"
|
||||
)
|
||||
webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id)
|
||||
webhook_provider = WebhookFactory.create_provider(
|
||||
webhook["webhook_type"]
|
||||
)
|
||||
if res is not False and swebhook.enabled:
|
||||
webhook_provider.send(
|
||||
bot_name=webhook["bot_name"],
|
||||
server_name=args[0].name,
|
||||
title=webhook["name"],
|
||||
url=webhook["url"],
|
||||
message=webhook["body"],
|
||||
color=webhook["color"],
|
||||
)
|
||||
return res
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class ServerOutBuf:
|
||||
lines = {}
|
||||
|
||||
@ -92,12 +136,13 @@ class ServerOutBuf:
|
||||
|
||||
# TODO: Do not send data to clients who do not have permission to view
|
||||
# this server's console
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": self.server_id},
|
||||
"vterm_new_line",
|
||||
{"line": highlighted + "<br />"},
|
||||
)
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": self.server_id},
|
||||
"vterm_new_line",
|
||||
{"line": highlighted + "<br />"},
|
||||
)
|
||||
|
||||
|
||||
# **********************************************************************************
|
||||
@ -133,6 +178,8 @@ class ServerInstance:
|
||||
self.server_object = HelperServers.get_server_obj(self.server_id)
|
||||
self.stats_helper = HelperServerStats(self.server_id)
|
||||
self.last_backup_failed = False
|
||||
self.server_registry = CollectorRegistry()
|
||||
|
||||
try:
|
||||
with open(
|
||||
os.path.join(self.server_object.path, "db_stats", "players_cache.json"),
|
||||
@ -152,6 +199,7 @@ class ServerInstance:
|
||||
self.tz = ZoneInfo("Europe/London")
|
||||
self.server_scheduler = BackgroundScheduler(timezone=str(self.tz))
|
||||
self.dir_scheduler = BackgroundScheduler(timezone=str(self.tz))
|
||||
self.init_registries()
|
||||
self.server_scheduler.start()
|
||||
self.dir_scheduler.start()
|
||||
self.start_dir_calc_task()
|
||||
@ -251,6 +299,23 @@ class ServerInstance:
|
||||
seconds=5,
|
||||
id="stats_" + str(self.server_id),
|
||||
)
|
||||
logger.info(f"Saving server statistics {self.name} every {30} seconds")
|
||||
Console.info(f"Saving server statistics {self.name} every {30} seconds")
|
||||
try:
|
||||
self.server_scheduler.add_job(
|
||||
self.record_server_stats,
|
||||
"interval",
|
||||
seconds=30,
|
||||
id="save_stats_" + str(self.server_id),
|
||||
)
|
||||
except ConflictingIdError:
|
||||
self.server_scheduler.remove_job("save_stats_" + str(self.server_id))
|
||||
self.server_scheduler.add_job(
|
||||
self.record_server_stats,
|
||||
"interval",
|
||||
seconds=30,
|
||||
id="save_stats_" + str(self.server_id),
|
||||
)
|
||||
|
||||
def setup_server_run_command(self):
|
||||
# configure the server
|
||||
@ -313,6 +378,7 @@ class ServerInstance:
|
||||
logger.critical(f"Unable to write/access {self.server_path}")
|
||||
Console.critical(f"Unable to write/access {self.server_path}")
|
||||
|
||||
@callback
|
||||
def start_server(self, user_id, forge_install=False):
|
||||
if not user_id:
|
||||
user_lang = self.helper.get_setting("language")
|
||||
@ -322,7 +388,7 @@ class ServerInstance:
|
||||
# Checks if user is currently attempting to move global server
|
||||
# dir
|
||||
if self.helper.dir_migration:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -337,7 +403,7 @@ class ServerInstance:
|
||||
|
||||
if self.stats_helper.get_import_status() and not forge_install:
|
||||
if user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -383,7 +449,7 @@ class ServerInstance:
|
||||
e_flag = True
|
||||
if not e_flag and self.settings["type"] == "minecraft-java":
|
||||
if user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id, "send_eula_bootbox", {"id": self.server_id}
|
||||
)
|
||||
else:
|
||||
@ -416,7 +482,7 @@ class ServerInstance:
|
||||
|
||||
except:
|
||||
if user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -452,7 +518,7 @@ class ServerInstance:
|
||||
f"Server {self.name} failed to start with error code: {ex}"
|
||||
)
|
||||
if user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -552,7 +618,7 @@ class ServerInstance:
|
||||
# Checks for java on initial fail
|
||||
if not self.helper.detect_java():
|
||||
if user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -566,7 +632,7 @@ class ServerInstance:
|
||||
f"Server {self.name} failed to start with error code: {ex}"
|
||||
)
|
||||
if user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -613,7 +679,7 @@ class ServerInstance:
|
||||
self.stats_helper.set_first_run()
|
||||
loc_server_port = self.stats_helper.get_server_stats()["server_port"]
|
||||
# Sends port reminder message.
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -625,15 +691,11 @@ class ServerInstance:
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
for user in server_users:
|
||||
if user != user_id:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
user, "send_start_reload", {}
|
||||
)
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
else:
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
user, "send_start_reload", {}
|
||||
)
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
else:
|
||||
logger.warning(
|
||||
f"Server PID {self.process.pid} died right after starting "
|
||||
@ -665,7 +727,7 @@ class ServerInstance:
|
||||
def check_internet_thread(self, user_id, user_lang):
|
||||
if user_id:
|
||||
if not Helpers.check_internet():
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -792,9 +854,7 @@ class ServerInstance:
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
user, "send_start_reload", {}
|
||||
)
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
break
|
||||
|
||||
def stop_crash_detection(self):
|
||||
@ -835,6 +895,7 @@ class ServerInstance:
|
||||
if self.server_thread:
|
||||
self.server_thread.join()
|
||||
|
||||
@callback
|
||||
def stop_server(self):
|
||||
running = self.check_running()
|
||||
if not running:
|
||||
@ -852,6 +913,7 @@ class ServerInstance:
|
||||
f"Assuming it was never started."
|
||||
)
|
||||
if self.settings["stop_command"]:
|
||||
logger.info(f"Stop command requested for {self.settings['server_name']}.")
|
||||
self.send_command(self.settings["stop_command"])
|
||||
self.write_player_cache()
|
||||
else:
|
||||
@ -907,7 +969,7 @@ class ServerInstance:
|
||||
self.record_server_stats()
|
||||
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
|
||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||
|
||||
def restart_threaded_server(self, user_id):
|
||||
bu_conf = HelpersManagement.get_backup_config(self.server_id)
|
||||
@ -921,6 +983,9 @@ class ServerInstance:
|
||||
if not self.check_running():
|
||||
self.run_threaded_server(user_id)
|
||||
else:
|
||||
logger.info(
|
||||
f"Restart command detected. Sending stop command to {self.server_id}."
|
||||
)
|
||||
self.stop_threaded_server()
|
||||
time.sleep(2)
|
||||
self.run_threaded_server(user_id)
|
||||
@ -942,6 +1007,7 @@ class ServerInstance:
|
||||
self.last_rc = poll
|
||||
return False
|
||||
|
||||
@callback
|
||||
def send_command(self, command):
|
||||
if not self.check_running() and command.lower() != "start":
|
||||
logger.warning(f'Server not running, unable to send command "{command}"')
|
||||
@ -954,6 +1020,7 @@ class ServerInstance:
|
||||
self.process.stdin.flush()
|
||||
return True
|
||||
|
||||
@callback
|
||||
def crash_detected(self, name):
|
||||
# clear the old scheduled watcher task
|
||||
self.server_scheduler.remove_job(f"c_{self.server_id}")
|
||||
@ -974,6 +1041,7 @@ class ServerInstance:
|
||||
f"The server {name} has crashed and will be restarted. "
|
||||
f"Restarting server"
|
||||
)
|
||||
|
||||
self.run_threaded_server(None)
|
||||
return True
|
||||
logger.critical(
|
||||
@ -986,6 +1054,7 @@ class ServerInstance:
|
||||
)
|
||||
return False
|
||||
|
||||
@callback
|
||||
def kill(self):
|
||||
logger.info(f"Terminating server {self.server_id} and all child processes")
|
||||
try:
|
||||
@ -1074,6 +1143,7 @@ class ServerInstance:
|
||||
f.write("eula=true")
|
||||
self.run_threaded_server(user_id)
|
||||
|
||||
@callback
|
||||
def backup_server(self):
|
||||
if self.settings["backup_path"] == "":
|
||||
logger.critical("Backup path is None. Canceling Backup!")
|
||||
@ -1107,18 +1177,11 @@ class ServerInstance:
|
||||
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
|
||||
|
||||
def a_backup_server(self):
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"backup_reload",
|
||||
{"percent": 0, "total_files": 0},
|
||||
)
|
||||
was_server_running = None
|
||||
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
self.helper.translation.translate(
|
||||
@ -1193,8 +1256,8 @@ class ServerInstance:
|
||||
self.is_backingup = False
|
||||
logger.info(f"Backup of server: {self.name} completed")
|
||||
results = {"percent": 100, "total_files": 0, "current_file": 0}
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"backup_status",
|
||||
@ -1202,7 +1265,7 @@ class ServerInstance:
|
||||
)
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
self.helper.translation.translate(
|
||||
@ -1231,8 +1294,8 @@ class ServerInstance:
|
||||
f"Failed to create backup of server {self.name} (ID {self.server_id})"
|
||||
)
|
||||
results = {"percent": 100, "total_files": 0, "current_file": 0}
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"backup_status",
|
||||
@ -1249,8 +1312,8 @@ class ServerInstance:
|
||||
def backup_status(self, source_path, dest_path):
|
||||
results = Helpers.calc_percent(source_path, dest_path)
|
||||
self.backup_stats = results
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"backup_status",
|
||||
@ -1295,6 +1358,7 @@ class ServerInstance:
|
||||
if f["path"].endswith(".zip")
|
||||
]
|
||||
|
||||
@callback
|
||||
def jar_update(self):
|
||||
self.stats_helper.set_update(True)
|
||||
update_thread = threading.Thread(
|
||||
@ -1353,14 +1417,14 @@ class ServerInstance:
|
||||
self.stop_threaded_server()
|
||||
else:
|
||||
was_started = False
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
# There are clients
|
||||
self.check_update()
|
||||
message = (
|
||||
'<a data-id="' + str(self.server_id) + '" class=""> UPDATING...</i></a>'
|
||||
)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user_page(
|
||||
WebSocketManager().broadcast_user_page(
|
||||
"/panel/server_detail",
|
||||
user,
|
||||
"update_button_status",
|
||||
@ -1413,7 +1477,7 @@ class ServerInstance:
|
||||
# check if backup was successful
|
||||
if self.last_backup_failed:
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
"Backup failed for " + self.name + ". canceling update.",
|
||||
@ -1459,11 +1523,11 @@ class ServerInstance:
|
||||
logger.info("Executable updated successfully. Starting Server")
|
||||
|
||||
self.stats_helper.set_update(False)
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
# There are clients
|
||||
self.check_update()
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
"Executable update finished for " + self.name,
|
||||
@ -1471,7 +1535,7 @@ class ServerInstance:
|
||||
# sleep so first notif can completely run
|
||||
time.sleep(3)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user_page(
|
||||
WebSocketManager().broadcast_user_page(
|
||||
"/panel/server_detail",
|
||||
user,
|
||||
"update_button_status",
|
||||
@ -1481,10 +1545,10 @@ class ServerInstance:
|
||||
"wasRunning": was_started,
|
||||
},
|
||||
)
|
||||
self.helper.websocket_helper.broadcast_user_page(
|
||||
WebSocketManager().broadcast_user_page(
|
||||
user, "/panel/dashboard", "send_start_reload", {}
|
||||
)
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
"Executable update finished for " + self.name,
|
||||
@ -1501,7 +1565,7 @@ class ServerInstance:
|
||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||
else:
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
"Executable update failed for "
|
||||
@ -1511,7 +1575,7 @@ class ServerInstance:
|
||||
logger.error("Executable download failed.")
|
||||
self.stats_helper.set_update(False)
|
||||
for user in server_users:
|
||||
self.helper.websocket_helper.broadcast_user(user, "remove_spinner", {})
|
||||
WebSocketManager().broadcast_user(user, "remove_spinner", {})
|
||||
|
||||
def start_dir_calc_task(self):
|
||||
server_dt = HelperServers.get_server_data_by_id(self.server_id)
|
||||
@ -1540,7 +1604,7 @@ class ServerInstance:
|
||||
def realtime_stats(self):
|
||||
# only get stats if clients are connected.
|
||||
# no point in burning cpu
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
total_players = 0
|
||||
max_players = 0
|
||||
servers_ping = []
|
||||
@ -1571,50 +1635,43 @@ class ServerInstance:
|
||||
"crashed": self.is_crashed,
|
||||
}
|
||||
)
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
self.helper.websocket_helper.broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"update_server_details",
|
||||
{
|
||||
"id": raw_ping_result.get("id"),
|
||||
"started": raw_ping_result.get("started"),
|
||||
"running": raw_ping_result.get("running"),
|
||||
"cpu": raw_ping_result.get("cpu"),
|
||||
"mem": raw_ping_result.get("mem"),
|
||||
"mem_percent": raw_ping_result.get("mem_percent"),
|
||||
"world_name": raw_ping_result.get("world_name"),
|
||||
"world_size": raw_ping_result.get("world_size"),
|
||||
"server_port": raw_ping_result.get("server_port"),
|
||||
"int_ping_results": raw_ping_result.get("int_ping_results"),
|
||||
"online": raw_ping_result.get("online"),
|
||||
"max": raw_ping_result.get("max"),
|
||||
"players": raw_ping_result.get("players"),
|
||||
"desc": raw_ping_result.get("desc"),
|
||||
"version": raw_ping_result.get("version"),
|
||||
"icon": raw_ping_result.get("icon"),
|
||||
"crashed": self.is_crashed,
|
||||
"created": datetime.datetime.now().strftime(
|
||||
"%Y/%m/%d, %H:%M:%S"
|
||||
),
|
||||
"players_cache": self.player_cache,
|
||||
},
|
||||
)
|
||||
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"update_server_details",
|
||||
{
|
||||
"id": raw_ping_result.get("id"),
|
||||
"started": raw_ping_result.get("started"),
|
||||
"running": raw_ping_result.get("running"),
|
||||
"cpu": raw_ping_result.get("cpu"),
|
||||
"mem": raw_ping_result.get("mem"),
|
||||
"mem_percent": raw_ping_result.get("mem_percent"),
|
||||
"world_name": raw_ping_result.get("world_name"),
|
||||
"world_size": raw_ping_result.get("world_size"),
|
||||
"server_port": raw_ping_result.get("server_port"),
|
||||
"int_ping_results": raw_ping_result.get("int_ping_results"),
|
||||
"online": raw_ping_result.get("online"),
|
||||
"max": raw_ping_result.get("max"),
|
||||
"players": raw_ping_result.get("players"),
|
||||
"desc": raw_ping_result.get("desc"),
|
||||
"version": raw_ping_result.get("version"),
|
||||
"icon": raw_ping_result.get("icon"),
|
||||
"crashed": self.is_crashed,
|
||||
"created": datetime.datetime.now().strftime("%Y/%m/%d, %H:%M:%S"),
|
||||
"players_cache": self.player_cache,
|
||||
},
|
||||
)
|
||||
total_players += int(raw_ping_result.get("online"))
|
||||
max_players += int(raw_ping_result.get("max"))
|
||||
|
||||
self.record_server_stats()
|
||||
# self.record_server_stats()
|
||||
|
||||
if (len(servers_ping) > 0) & (
|
||||
len(self.helper.websocket_helper.clients) > 0
|
||||
):
|
||||
if len(servers_ping) > 0:
|
||||
try:
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/dashboard", "update_server_status", servers_ping
|
||||
)
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
"/status", "update_server_status", servers_ping
|
||||
)
|
||||
except:
|
||||
Console.critical("Can't broadcast server status to websocket")
|
||||
|
||||
@ -1633,7 +1690,6 @@ class ServerInstance:
|
||||
|
||||
# process stats
|
||||
p_stats = Stats._try_get_process_stats(self.process, self.check_running())
|
||||
|
||||
internal_ip = server["server_ip"]
|
||||
server_port = server["server_port"]
|
||||
server_name = server.get("server_name", f"ID#{server_id}")
|
||||
@ -1683,6 +1739,7 @@ class ServerInstance:
|
||||
"players": ping_data.get("players", False),
|
||||
"desc": ping_data.get("server_description", False),
|
||||
"version": ping_data.get("server_version", False),
|
||||
"icon": ping_data.get("server_icon"),
|
||||
}
|
||||
else:
|
||||
server_stats = {
|
||||
@ -1701,6 +1758,7 @@ class ServerInstance:
|
||||
"players": False,
|
||||
"desc": False,
|
||||
"version": False,
|
||||
"icon": None,
|
||||
}
|
||||
|
||||
return server_stats
|
||||
@ -1708,7 +1766,7 @@ class ServerInstance:
|
||||
def get_server_players(self):
|
||||
server = HelperServers.get_server_data_by_id(self.server_id)
|
||||
|
||||
logger.info(f"Getting players for server {server}")
|
||||
logger.debug(f"Getting players for server {server['server_name']}")
|
||||
|
||||
internal_ip = server["server_ip"]
|
||||
server_port = server["server_port"]
|
||||
@ -1749,7 +1807,6 @@ class ServerInstance:
|
||||
}
|
||||
|
||||
server_stats = {}
|
||||
server = HelperServers.get_server_obj(server_id)
|
||||
if not server:
|
||||
return {}
|
||||
server_dt = HelperServers.get_server_data_by_id(server_id)
|
||||
@ -1879,9 +1936,50 @@ class ServerInstance:
|
||||
server_stats = self.get_servers_stats()
|
||||
self.stats_helper.insert_server_stats(server_stats)
|
||||
|
||||
self.cpu_usage.labels(f"{self.server_id}").set(server_stats.get("cpu"))
|
||||
self.mem_usage_percent.labels(f"{self.server_id}").set(
|
||||
server_stats.get("mem_percent")
|
||||
)
|
||||
self.minecraft_version.labels(f"{self.server_id}").info(
|
||||
{"version": f"{server_stats.get('version')}"}
|
||||
)
|
||||
self.online_players.labels(f"{self.server_id}").set(server_stats.get("online"))
|
||||
|
||||
# delete old data
|
||||
max_age = self.helper.get_setting("history_max_age")
|
||||
now = datetime.datetime.now()
|
||||
minimum_to_exist = now - datetime.timedelta(days=max_age)
|
||||
|
||||
self.stats_helper.remove_old_stats(minimum_to_exist)
|
||||
|
||||
def init_registries(self):
|
||||
# REGISTRY Entries for Server Stats functions
|
||||
self.cpu_usage = Gauge(
|
||||
name="CPU_Usage",
|
||||
documentation="The CPU usage of the server",
|
||||
labelnames=["server_id"],
|
||||
registry=self.server_registry,
|
||||
)
|
||||
self.mem_usage_percent = Gauge(
|
||||
name="Mem_Usage",
|
||||
documentation="The Memory usage of the server",
|
||||
labelnames=["server_id"],
|
||||
registry=self.server_registry,
|
||||
)
|
||||
self.minecraft_version = Info(
|
||||
name="Minecraft_Version",
|
||||
documentation="The version of the minecraft of this server",
|
||||
labelnames=["server_id"],
|
||||
registry=self.server_registry,
|
||||
)
|
||||
|
||||
self.online_players = Gauge(
|
||||
name="online_players",
|
||||
documentation="The number of players online for a server",
|
||||
labelnames=["server_id"],
|
||||
registry=self.server_registry,
|
||||
)
|
||||
|
||||
def get_server_history(self):
|
||||
history = self.stats_helper.get_history_stats(self.server_id, 1)
|
||||
return history
|
||||
|
@ -5,10 +5,10 @@ import threading
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from zoneinfo import ZoneInfoNotFoundError
|
||||
from tzlocal import get_localzone
|
||||
from tzlocal.utils import ZoneInfoNotFoundError
|
||||
from apscheduler.events import EVENT_JOB_EXECUTED
|
||||
from apscheduler.jobstores.base import JobLookupError
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
@ -20,6 +20,7 @@ from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.web.tornado_handler import Webserver
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger("apscheduler")
|
||||
scheduler_intervals = {
|
||||
@ -101,7 +102,7 @@ class TasksManager:
|
||||
)
|
||||
except:
|
||||
logger.error(
|
||||
"Server value requested does not exist! "
|
||||
f"Server value {cmd['server_id']} requested does not exist! "
|
||||
"Purging item from waiting commands."
|
||||
)
|
||||
continue
|
||||
@ -200,6 +201,13 @@ class TasksManager:
|
||||
id="update_watcher",
|
||||
start_date=datetime.datetime.now(),
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
self.controller.write_auth_tracker,
|
||||
"interval",
|
||||
minutes=5,
|
||||
id="auth_tracker_write",
|
||||
start_date=datetime.datetime.now(),
|
||||
)
|
||||
# self.scheduler.add_job(
|
||||
# self.scheduler.print_jobs, "interval", seconds=10, id="-1"
|
||||
# )
|
||||
@ -324,11 +332,16 @@ class TasksManager:
|
||||
|
||||
# Checks to make sure some doofus didn't actually make the newly
|
||||
# created task a child of itself.
|
||||
if str(job_data["parent"]) == str(sch_id):
|
||||
if (
|
||||
str(job_data["parent"]) == str(sch_id)
|
||||
or job_data["interval_type"] != "reaction"
|
||||
):
|
||||
HelpersManagement.update_scheduled_task(sch_id, {"parent": None})
|
||||
|
||||
# Check to see if it's enabled and is not a chain reaction.
|
||||
if job_data["enabled"] and job_data["interval_type"] != "reaction":
|
||||
# Lets make sure this can not be mistaken for a reaction
|
||||
job_data["parent"] = None
|
||||
new_job = "error"
|
||||
if job_data["cron_string"] != "":
|
||||
try:
|
||||
@ -449,7 +462,8 @@ class TasksManager:
|
||||
def update_job(self, sch_id, job_data):
|
||||
# Checks to make sure some doofus didn't actually make the newly
|
||||
# created task a child of itself.
|
||||
if str(job_data.get("parent")) == str(sch_id):
|
||||
interval_type = job_data.get("interval_type")
|
||||
if str(job_data.get("parent")) == str(sch_id) or interval_type != "reaction":
|
||||
job_data["parent"] = None
|
||||
HelpersManagement.update_scheduled_task(sch_id, job_data)
|
||||
|
||||
@ -466,13 +480,15 @@ class TasksManager:
|
||||
job_data = HelpersManagement.get_scheduled_task(sch_id)
|
||||
job_data["server_id"] = job_data["server_id"]["server_id"]
|
||||
else:
|
||||
self.scheduler.remove_job(str(sch_id))
|
||||
job = HelpersManagement.get_scheduled_task(sch_id)
|
||||
if job["interval_type"] != "reaction":
|
||||
self.scheduler.remove_job(str(sch_id))
|
||||
return
|
||||
|
||||
try:
|
||||
if job_data["interval"] != "reaction":
|
||||
self.scheduler.remove_job(str(sch_id))
|
||||
except:
|
||||
except JobLookupError:
|
||||
logger.info(
|
||||
"No job found in update job. "
|
||||
"Assuming it was previously disabled. Starting new job."
|
||||
@ -608,7 +624,10 @@ class TasksManager:
|
||||
):
|
||||
# event job ID's are strings so we need to look at
|
||||
# this as the same data type.
|
||||
if str(schedule.parent) == str(event.job_id):
|
||||
if (
|
||||
str(schedule.parent) == str(event.job_id)
|
||||
and schedule.interval_type == "reaction"
|
||||
):
|
||||
if schedule.enabled:
|
||||
delaytime = datetime.datetime.now() + datetime.timedelta(
|
||||
seconds=schedule.delay
|
||||
@ -700,10 +719,16 @@ class TasksManager:
|
||||
# Stats are different
|
||||
|
||||
host_stats = HelpersManagement.get_latest_hosts_stats()
|
||||
if len(self.helper.websocket_helper.clients) > 0:
|
||||
|
||||
self.controller.management.cpu_usage.set(host_stats.get("cpu_usage"))
|
||||
self.controller.management.mem_usage_percent.set(
|
||||
host_stats.get("mem_percent")
|
||||
)
|
||||
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
# There are clients
|
||||
try:
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/dashboard",
|
||||
"update_host_stats",
|
||||
{
|
||||
@ -720,7 +745,7 @@ class TasksManager:
|
||||
},
|
||||
)
|
||||
except:
|
||||
self.helper.websocket_helper.broadcast_page(
|
||||
WebSocketManager().broadcast_page(
|
||||
"/panel/dashboard",
|
||||
"update_host_stats",
|
||||
{
|
||||
@ -761,11 +786,13 @@ class TasksManager:
|
||||
)
|
||||
# Search for old files in imports
|
||||
self.helper.ensure_dir_exists(
|
||||
os.path.join(self.controller.project_root, "imports")
|
||||
os.path.join(self.controller.project_root, "import", "upload")
|
||||
)
|
||||
for file in os.listdir(os.path.join(self.controller.project_root, "imports")):
|
||||
for file in os.listdir(
|
||||
os.path.join(self.controller.project_root, "import", "upload")
|
||||
):
|
||||
if self.helper.is_file_older_than_x_days(
|
||||
os.path.join(self.controller.project_root, "imports", file)
|
||||
os.path.join(self.controller.project_root, "import", "upload", file)
|
||||
):
|
||||
try:
|
||||
os.remove(os.path.join(file))
|
||||
|
@ -1,26 +1,25 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.classes.shared.singleton import Singleton
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.models.users import HelperUsers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketHelper:
|
||||
def __init__(self, helper):
|
||||
self.helper = helper
|
||||
class WebSocketManager(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
self.clients = set()
|
||||
|
||||
def add_client(self, client):
|
||||
self.clients.add(client)
|
||||
|
||||
def remove_client(self, client):
|
||||
self.clients.remove(client)
|
||||
|
||||
def send_message(self, client, event_type: str, data):
|
||||
if client.check_auth():
|
||||
message = str(json.dumps({"event": event_type, "data": data}))
|
||||
client.write_message_helper(message)
|
||||
if client in self.clients:
|
||||
self.clients.remove(client)
|
||||
else:
|
||||
logger.exception("Error caught while removing unknown WebSocket client")
|
||||
|
||||
def broadcast(self, event_type: str, data):
|
||||
logger.debug(
|
||||
@ -29,13 +28,29 @@ class WebSocketHelper:
|
||||
)
|
||||
for client in self.clients:
|
||||
try:
|
||||
self.send_message(client, event_type, data)
|
||||
client.send_message(event_type, data)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error caught while sending WebSocket message to "
|
||||
f"{client.get_remote_ip()} {e}"
|
||||
)
|
||||
|
||||
def broadcast_to_admins(self, event_type: str, data):
|
||||
def filter_fn(client):
|
||||
if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()):
|
||||
return True
|
||||
return False
|
||||
|
||||
self.broadcast_with_fn(filter_fn, event_type, data)
|
||||
|
||||
def broadcast_to_non_admins(self, event_type: str, data):
|
||||
def filter_fn(client):
|
||||
if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()):
|
||||
return True
|
||||
return False
|
||||
|
||||
self.broadcast_with_fn(filter_fn, event_type, data)
|
||||
|
||||
def broadcast_page(self, page: str, event_type: str, data):
|
||||
def filter_fn(client):
|
||||
return client.page == page
|
||||
@ -90,13 +105,14 @@ class WebSocketHelper:
|
||||
static_clients = self.clients
|
||||
clients = list(filter(filter_fn, static_clients))
|
||||
logger.debug(
|
||||
f"Sending to {len(clients)} out of {len(self.clients)} "
|
||||
f"Sending to {len(clients)} \
|
||||
out of {len(self.clients)} "
|
||||
f"clients: {json.dumps({'event': event_type, 'data': data})}"
|
||||
)
|
||||
|
||||
for client in clients[:]:
|
||||
try:
|
||||
self.send_message(client, event_type, data)
|
||||
client.send_message(event_type, data)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error catched while sending WebSocket message to "
|
@ -1,446 +0,0 @@
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
from app.classes.controllers.crafty_perms_controller import EnumPermissionsCrafty
|
||||
from app.classes.controllers.server_perms_controller import EnumPermissionsServer
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
from app.classes.models.management import DatabaseShortcuts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
bearer_pattern = re.compile(r"^Bearer", flags=re.IGNORECASE)
|
||||
|
||||
|
||||
class ApiHandler(BaseHandler):
|
||||
def return_response(self, status: int, data: dict):
|
||||
# Define a standardized response
|
||||
self.set_status(status)
|
||||
self.write(data)
|
||||
|
||||
def check_xsrf_cookie(self):
|
||||
# Disable CSRF protection on API routes
|
||||
pass
|
||||
|
||||
def access_denied(self, user, reason=""):
|
||||
if reason:
|
||||
reason = " because " + reason
|
||||
logger.info(
|
||||
"User %s from IP %s was denied access to the API route "
|
||||
+ self.request.path
|
||||
+ reason,
|
||||
user,
|
||||
self.get_remote_ip(),
|
||||
)
|
||||
self.finish(
|
||||
self.return_response(
|
||||
403,
|
||||
{
|
||||
"error": "ACCESS_DENIED",
|
||||
"info": "You were denied access to the requested resource",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def authenticate_user(self) -> bool:
|
||||
self.permissions = {
|
||||
"Commands": EnumPermissionsServer.COMMANDS,
|
||||
"Terminal": EnumPermissionsServer.TERMINAL,
|
||||
"Logs": EnumPermissionsServer.LOGS,
|
||||
"Schedule": EnumPermissionsServer.SCHEDULE,
|
||||
"Backup": EnumPermissionsServer.BACKUP,
|
||||
"Files": EnumPermissionsServer.FILES,
|
||||
"Config": EnumPermissionsServer.CONFIG,
|
||||
"Players": EnumPermissionsServer.PLAYERS,
|
||||
"Server_Creation": EnumPermissionsCrafty.SERVER_CREATION,
|
||||
"User_Config": EnumPermissionsCrafty.USER_CONFIG,
|
||||
"Roles_Config": EnumPermissionsCrafty.ROLES_CONFIG,
|
||||
}
|
||||
try:
|
||||
logger.debug("Searching for specified token")
|
||||
|
||||
api_token = self.get_argument("token", "")
|
||||
self.api_token = api_token
|
||||
if api_token is None and self.request.headers.get("Authorization"):
|
||||
api_token = bearer_pattern.sub(
|
||||
"", self.request.headers.get("Authorization")
|
||||
)
|
||||
elif api_token is None:
|
||||
api_token = self.get_cookie("token")
|
||||
user_data = self.controller.users.get_user_by_api_token(api_token)
|
||||
|
||||
logger.debug("Checking results")
|
||||
if user_data:
|
||||
# Login successful! Check perms
|
||||
logger.info(f"User {user_data['username']} has authenticated to API")
|
||||
|
||||
return True # This is to set the "authenticated"
|
||||
logging.debug("Auth unsuccessful")
|
||||
self.access_denied("unknown", "the user provided an invalid token")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("An error occured while authenticating an API user: %s", e)
|
||||
self.finish(
|
||||
self.return_response(
|
||||
403,
|
||||
{
|
||||
"error": "ACCESS_DENIED",
|
||||
"info": "An error occured while authenticating the user",
|
||||
},
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class ServersStats(ApiHandler):
|
||||
def get(self):
|
||||
"""Get details about all servers"""
|
||||
authenticated = self.authenticate_user()
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
if not authenticated:
|
||||
return
|
||||
if user_obj["superuser"]:
|
||||
raw_stats = self.controller.servers.get_all_servers_stats()
|
||||
else:
|
||||
raw_stats = self.controller.servers.get_authorized_servers_stats(
|
||||
user_obj["user_id"]
|
||||
)
|
||||
stats = []
|
||||
for rs in raw_stats:
|
||||
s = {}
|
||||
for k, v in rs["server_data"].items():
|
||||
if isinstance(v, datetime):
|
||||
s[k] = v.timestamp()
|
||||
else:
|
||||
s[k] = v
|
||||
stats.append(s)
|
||||
|
||||
# Get server stats
|
||||
# TODO Check perms
|
||||
self.finish(self.write({"servers": stats}))
|
||||
|
||||
|
||||
class NodeStats(ApiHandler):
|
||||
def get(self):
|
||||
"""Get stats for particular node"""
|
||||
authenticated = self.authenticate_user()
|
||||
if not authenticated:
|
||||
return
|
||||
|
||||
# Get node stats
|
||||
node_stats = self.controller.servers.stats.get_node_stats()
|
||||
self.return_response(200, {"code": node_stats["node_stats"]})
|
||||
|
||||
|
||||
class SendCommand(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
server_id = self.get_argument("id")
|
||||
|
||||
if (
|
||||
not user_obj["user_id"]
|
||||
in self.controller.server_perms.get_server_user_list(server_id)
|
||||
and not user_obj["superuser"]
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if not self.permissions[
|
||||
"Commands"
|
||||
] in self.controller.server_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
||||
):
|
||||
self.access_denied(user)
|
||||
return
|
||||
|
||||
command = self.get_argument("command", default=None, strip=True)
|
||||
server_id = self.get_argument("id")
|
||||
if command:
|
||||
server = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
if server.check_running:
|
||||
server.send_command(command)
|
||||
self.return_response(200, {"run": True})
|
||||
else:
|
||||
self.return_response(200, {"error": "SER_NOT_RUNNING"})
|
||||
else:
|
||||
self.return_response(200, {"error": "NO_COMMAND"})
|
||||
|
||||
|
||||
class ServerBackup(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
server_id = self.get_argument("id")
|
||||
|
||||
if (
|
||||
not user_obj["user_id"]
|
||||
in self.controller.server_perms.get_server_user_list(server_id)
|
||||
and not user_obj["superuser"]
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if not self.permissions[
|
||||
"Backup"
|
||||
] in self.controller.server_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
||||
):
|
||||
self.access_denied(user)
|
||||
return
|
||||
|
||||
server = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
|
||||
server.backup_server()
|
||||
|
||||
self.return_response(200, {"code": "SER_BAK_CALLED"})
|
||||
|
||||
|
||||
class StartServer(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
remote_ip = self.get_remote_ip()
|
||||
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
server_id = self.get_argument("id")
|
||||
|
||||
if (
|
||||
not user_obj["user_id"]
|
||||
in self.controller.server_perms.get_server_user_list(server_id)
|
||||
and not user_obj["superuser"]
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
if not self.permissions[
|
||||
"Commands"
|
||||
] in self.controller.server_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
server = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
|
||||
if not server.check_running():
|
||||
self.controller.management.send_command(
|
||||
user_obj["user_id"], server_id, remote_ip, "start_server"
|
||||
)
|
||||
self.return_response(200, {"code": "SER_START_CALLED"})
|
||||
else:
|
||||
self.return_response(500, {"error": "SER_RUNNING"})
|
||||
|
||||
|
||||
class StopServer(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
remote_ip = self.get_remote_ip()
|
||||
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
server_id = self.get_argument("id")
|
||||
|
||||
if (
|
||||
not user_obj["user_id"]
|
||||
in self.controller.server_perms.get_server_user_list(server_id)
|
||||
and not user_obj["superuser"]
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
|
||||
if not self.permissions[
|
||||
"Commands"
|
||||
] in self.controller.server_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
||||
):
|
||||
self.access_denied(user)
|
||||
return
|
||||
|
||||
server = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
|
||||
if server.check_running():
|
||||
self.controller.management.send_command(
|
||||
user, server_id, remote_ip, "stop_server"
|
||||
)
|
||||
|
||||
self.return_response(200, {"code": "SER_STOP_CALLED"})
|
||||
else:
|
||||
self.return_response(500, {"error": "SER_NOT_RUNNING"})
|
||||
|
||||
|
||||
class RestartServer(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
remote_ip = self.get_remote_ip()
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
server_id = self.get_argument("id")
|
||||
|
||||
if not user_obj["user_id"] in self.controller.server_perms.get_server_user_list(
|
||||
server_id
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
|
||||
if not self.permissions[
|
||||
"Commands"
|
||||
] in self.controller.server_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
||||
):
|
||||
self.access_denied(user)
|
||||
|
||||
self.controller.management.send_command(
|
||||
user, server_id, remote_ip, "restart_server"
|
||||
)
|
||||
self.return_response(200, {"code": "SER_RESTART_CALLED"})
|
||||
|
||||
|
||||
class CreateUser(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
|
||||
user_obj["user_id"]
|
||||
)
|
||||
if (
|
||||
not self.permissions["User_Config"] in user_perms
|
||||
and not user_obj["superuser"]
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if not self.permissions[
|
||||
"User_Config"
|
||||
] in self.controller.crafty_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token)
|
||||
):
|
||||
self.access_denied(user)
|
||||
return
|
||||
|
||||
new_username = self.get_argument("username").lower()
|
||||
new_pass = self.get_argument("password")
|
||||
manager = int(user_obj["user_id"])
|
||||
|
||||
if new_username:
|
||||
self.controller.users.add_user(
|
||||
new_username, manager, new_pass, "default@example.com", True, False
|
||||
)
|
||||
|
||||
self.return_response(
|
||||
200,
|
||||
{
|
||||
"code": "COMPLETE",
|
||||
"username": new_username,
|
||||
"password": new_pass,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.return_response(
|
||||
500,
|
||||
{
|
||||
"error": "MISSING_PARAMS",
|
||||
"info": "Some paramaters failed validation",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class DeleteUser(ApiHandler):
|
||||
def post(self):
|
||||
user = self.authenticate_user()
|
||||
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
|
||||
user_obj["user_id"]
|
||||
)
|
||||
|
||||
if (
|
||||
not self.permissions["User_Config"] in user_perms
|
||||
and not user_obj["superuser"]
|
||||
):
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if not self.permissions[
|
||||
"User_Config"
|
||||
] in self.controller.crafty_perms.get_api_key_permissions_list(
|
||||
self.controller.users.get_api_key_by_token(self.api_token)
|
||||
):
|
||||
self.access_denied(user)
|
||||
return
|
||||
|
||||
user_id = self.get_argument("user_id", None, True)
|
||||
user_to_del = self.controller.users.get_user_by_id(user_id)
|
||||
|
||||
if user_to_del["superuser"]:
|
||||
self.return_response(
|
||||
500,
|
||||
{"error": "NOT_ALLOWED", "info": "You cannot delete a super user"},
|
||||
)
|
||||
else:
|
||||
if user_id:
|
||||
self.controller.users.remove_user(user_id)
|
||||
self.return_response(200, {"code": "COMPLETED"})
|
||||
|
||||
|
||||
class ListServers(ApiHandler):
|
||||
def get(self):
|
||||
user = self.authenticate_user()
|
||||
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
||||
|
||||
if user is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if self.api_token is None:
|
||||
self.access_denied("unknown")
|
||||
return
|
||||
|
||||
if user_obj["superuser"]:
|
||||
servers = self.controller.servers.get_all_defined_servers()
|
||||
servers = [str(i) for i in servers]
|
||||
else:
|
||||
servers = self.controller.servers.get_authorized_servers(
|
||||
user_obj["user_id"]
|
||||
)
|
||||
page_servers = []
|
||||
for server in servers:
|
||||
if server not in page_servers:
|
||||
page_servers.append(
|
||||
DatabaseShortcuts.get_data_obj(server.server_object)
|
||||
)
|
||||
servers = page_servers
|
||||
servers = [str(i) for i in servers]
|
||||
|
||||
self.return_response(
|
||||
200,
|
||||
{
|
||||
"code": "COMPLETED",
|
||||
"servers": servers,
|
||||
},
|
||||
)
|
@ -2,7 +2,7 @@ import logging
|
||||
import re
|
||||
import typing as t
|
||||
import orjson
|
||||
import bleach
|
||||
import nh3
|
||||
import tornado.web
|
||||
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
@ -11,9 +11,10 @@ from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.shared.translation import Translation
|
||||
from app.classes.models.management import DatabaseShortcuts
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
auth_log = logging.getLogger("auth")
|
||||
|
||||
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
|
||||
|
||||
@ -101,7 +102,7 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
if type(text) in self.nobleach:
|
||||
logger.debug("Auto-bleaching - bypass type")
|
||||
return text
|
||||
return bleach.clean(text)
|
||||
return nh3.clean(text)
|
||||
|
||||
def get_argument(
|
||||
self,
|
||||
@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
user,
|
||||
)
|
||||
logging.debug("Auth unsuccessful")
|
||||
auth_log.error(
|
||||
f"Authentication attempted from {self.get_remote_ip()}. Invalid token"
|
||||
)
|
||||
self.access_denied(None, "the user provided an invalid token")
|
||||
return None
|
||||
except Exception as auth_exception:
|
||||
auth_log.error(
|
||||
f"Authentication attempted from {self.get_remote_ip()}."
|
||||
f" Error: {auth_exception}"
|
||||
)
|
||||
logger.debug(
|
||||
"An error occured while authenticating an API user:",
|
||||
exc_info=auth_exception,
|
||||
|
53
app/classes/web/metrics_handler.py
Normal file
@ -0,0 +1,53 @@
|
||||
import logging
|
||||
import typing as t
|
||||
|
||||
from prometheus_client import REGISTRY, CollectorRegistry
|
||||
from prometheus_client.exposition import _bake_output
|
||||
from prometheus_client.exposition import parse_qs, urlparse
|
||||
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseMetricsHandler(BaseApiHandler):
|
||||
"""HTTP handler that gives metrics from ``REGISTRY``."""
|
||||
|
||||
registry: CollectorRegistry = REGISTRY
|
||||
# registry.unregister(GC_COLLECTOR)
|
||||
# registry.unregister(PLATFORM_COLLECTOR)
|
||||
# registry.unregister(PROCESS_COLLECTOR)
|
||||
|
||||
def get_registry(self) -> None:
|
||||
# Prepare parameters
|
||||
registry = self.registry
|
||||
accept_header = self.request.headers.get("Accept")
|
||||
accept_encoding_header = self.request.headers.get("Accept-Encoding")
|
||||
params = parse_qs(urlparse(self.request.path).query)
|
||||
# Bake output
|
||||
status, headers, output = _bake_output(
|
||||
registry, accept_header, accept_encoding_header, params, False
|
||||
)
|
||||
# Return output
|
||||
self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output)
|
||||
|
||||
@classmethod
|
||||
def factory(cls, registry: CollectorRegistry) -> type:
|
||||
"""Returns a dynamic MetricsHandler class tied
|
||||
to the passed registry.
|
||||
"""
|
||||
# This implementation relies on MetricsHandler.registry
|
||||
# (defined above and defaulted to REGISTRY).
|
||||
|
||||
# As we have unicode_literals, we need to create a str()
|
||||
# object for type().
|
||||
cls_name = str(cls.__name__)
|
||||
MyMetricsHandler = type(cls_name, (cls, object), {"registry": registry})
|
||||
return MyMetricsHandler
|
||||
|
||||
def finish_metrics(self, status: int, headers, data: t.Dict[str, t.Any]):
|
||||
self.set_status(status)
|
||||
self.set_header("Content-Type", "text/plain")
|
||||
for header in headers:
|
||||
self.set_header(*header)
|
||||
self.finish(data)
|
@ -7,7 +7,8 @@ import json
|
||||
import logging
|
||||
import threading
|
||||
import urllib.parse
|
||||
import bleach
|
||||
from zoneinfo import ZoneInfoNotFoundError
|
||||
import nh3
|
||||
import requests
|
||||
import tornado.web
|
||||
import tornado.escape
|
||||
@ -15,7 +16,6 @@ from tornado import iostream
|
||||
|
||||
# TZLocal is set as a hidden import on win pipeline
|
||||
from tzlocal import get_localzone
|
||||
from tzlocal.utils import ZoneInfoNotFoundError
|
||||
|
||||
from app.classes.models.servers import Servers
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
@ -25,6 +25,7 @@ from app.classes.controllers.roles_controller import RolesController
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -67,9 +68,7 @@ class PanelHandler(BaseHandler):
|
||||
) in self.controller.crafty_perms.list_defined_crafty_permissions():
|
||||
argument = int(
|
||||
float(
|
||||
bleach.clean(
|
||||
self.get_argument(f"permission_{permission.name}", "0")
|
||||
)
|
||||
nh3.clean(self.get_argument(f"permission_{permission.name}", "0"))
|
||||
)
|
||||
)
|
||||
if argument:
|
||||
@ -78,9 +77,7 @@ class PanelHandler(BaseHandler):
|
||||
)
|
||||
|
||||
q_argument = int(
|
||||
float(
|
||||
bleach.clean(self.get_argument(f"quantity_{permission.name}", "0"))
|
||||
)
|
||||
float(nh3.clean(self.get_argument(f"quantity_{permission.name}", "0")))
|
||||
)
|
||||
if q_argument:
|
||||
server_quantity[permission.name] = q_argument
|
||||
@ -213,6 +210,8 @@ class PanelHandler(BaseHandler):
|
||||
error = self.get_argument("error", "WTF Error!")
|
||||
|
||||
template = "panel/denied.html"
|
||||
if self.helper.crafty_starting:
|
||||
page = "loading"
|
||||
|
||||
now = time.time()
|
||||
formatted_time = str(
|
||||
@ -246,9 +245,13 @@ class PanelHandler(BaseHandler):
|
||||
for r in exec_user["roles"]:
|
||||
role = self.controller.roles.get_role(r)
|
||||
exec_user_role.add(role["role_name"])
|
||||
defined_servers = self.controller.servers.get_authorized_servers(
|
||||
exec_user["user_id"]
|
||||
)
|
||||
# get_auth_servers will throw an exception if run while Crafty is starting
|
||||
if not self.helper.crafty_starting:
|
||||
defined_servers = self.controller.servers.get_authorized_servers(
|
||||
exec_user["user_id"]
|
||||
)
|
||||
else:
|
||||
defined_servers = []
|
||||
|
||||
user_order = self.controller.users.get_user_by_id(exec_user["user_id"])
|
||||
user_order = user_order["server_order"].split(",")
|
||||
@ -348,7 +351,9 @@ class PanelHandler(BaseHandler):
|
||||
) as credits_default_local:
|
||||
try:
|
||||
remote = requests.get(
|
||||
"https://craftycontrol.com/credits-v2", allow_redirects=True
|
||||
"https://craftycontrol.com/credits-v2",
|
||||
allow_redirects=True,
|
||||
timeout=10,
|
||||
)
|
||||
credits_dict: dict = remote.json()
|
||||
if not credits_dict["staff"]:
|
||||
@ -479,9 +484,15 @@ class PanelHandler(BaseHandler):
|
||||
template = "panel/dashboard.html"
|
||||
|
||||
elif page == "server_detail":
|
||||
subpage = bleach.clean(self.get_argument("subpage", ""))
|
||||
subpage = nh3.clean(self.get_argument("subpage", ""))
|
||||
|
||||
server_id = self.check_server_id()
|
||||
# load page the user was on last
|
||||
server_subpage = self.controller.servers.server_subpage.get(server_id, "")
|
||||
if subpage == "" and server_subpage != "":
|
||||
subpage = self.controller.servers.server_subpage.get(server_id, "")
|
||||
else:
|
||||
self.controller.servers.server_subpage[server_id] = subpage
|
||||
if server_id is None:
|
||||
return
|
||||
if not self.failed_server:
|
||||
@ -747,8 +758,24 @@ class PanelHandler(BaseHandler):
|
||||
0, page_data["options"].pop(page_data["options"].index(days))
|
||||
)
|
||||
page_data["history_stats"] = self.controller.servers.get_history_stats(
|
||||
server_id, days
|
||||
server_id, hours=(days * 24)
|
||||
)
|
||||
if subpage == "webhooks":
|
||||
if (
|
||||
not page_data["permissions"]["Config"]
|
||||
in page_data["user_permissions"]
|
||||
):
|
||||
if not superuser:
|
||||
self.redirect(
|
||||
"/panel/error?error=Unauthorized access to Webhooks Config"
|
||||
)
|
||||
return
|
||||
page_data[
|
||||
"webhooks"
|
||||
] = self.controller.management.get_webhooks_by_server(
|
||||
server_id, model=True
|
||||
)
|
||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
||||
|
||||
def get_banned_players_html():
|
||||
banned_players = self.controller.servers.get_banned_players(server_id)
|
||||
@ -1016,6 +1043,110 @@ class PanelHandler(BaseHandler):
|
||||
|
||||
template = "panel/panel_edit_user.html"
|
||||
|
||||
elif page == "add_webhook":
|
||||
server_id = self.get_argument("id", None)
|
||||
if server_id is None:
|
||||
return self.redirect("/panel/error?error=Invalid Server ID")
|
||||
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
page_data["backup_failed"] = server_obj.last_backup_status()
|
||||
server_obj = None
|
||||
page_data["active_link"] = "webhooks"
|
||||
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
||||
server_id
|
||||
)
|
||||
page_data[
|
||||
"user_permissions"
|
||||
] = self.controller.server_perms.get_user_id_permissions_list(
|
||||
exec_user["user_id"], server_id
|
||||
)
|
||||
page_data["permissions"] = {
|
||||
"Commands": EnumPermissionsServer.COMMANDS,
|
||||
"Terminal": EnumPermissionsServer.TERMINAL,
|
||||
"Logs": EnumPermissionsServer.LOGS,
|
||||
"Schedule": EnumPermissionsServer.SCHEDULE,
|
||||
"Backup": EnumPermissionsServer.BACKUP,
|
||||
"Files": EnumPermissionsServer.FILES,
|
||||
"Config": EnumPermissionsServer.CONFIG,
|
||||
"Players": EnumPermissionsServer.PLAYERS,
|
||||
}
|
||||
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
|
||||
server_id
|
||||
)
|
||||
page_data["server_stats"][
|
||||
"server_type"
|
||||
] = self.controller.servers.get_server_type_by_id(server_id)
|
||||
page_data["new_webhook"] = True
|
||||
page_data["webhook"] = {}
|
||||
page_data["webhook"]["webhook_type"] = "Custom"
|
||||
page_data["webhook"]["name"] = ""
|
||||
page_data["webhook"]["url"] = ""
|
||||
page_data["webhook"]["bot_name"] = "Crafty Controller"
|
||||
page_data["webhook"]["trigger"] = []
|
||||
page_data["webhook"]["body"] = ""
|
||||
page_data["webhook"]["color"] = "#005cd1"
|
||||
page_data["webhook"]["enabled"] = True
|
||||
|
||||
page_data["providers"] = WebhookFactory.get_supported_providers()
|
||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
||||
|
||||
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
||||
if not superuser:
|
||||
self.redirect("/panel/error?error=Unauthorized access To Webhooks")
|
||||
return
|
||||
|
||||
template = "panel/server_webhook_edit.html"
|
||||
|
||||
elif page == "webhook_edit":
|
||||
server_id = self.get_argument("id", None)
|
||||
webhook_id = self.get_argument("webhook_id", None)
|
||||
if server_id is None:
|
||||
return self.redirect("/panel/error?error=Invalid Server ID")
|
||||
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
page_data["backup_failed"] = server_obj.last_backup_status()
|
||||
server_obj = None
|
||||
page_data["active_link"] = "webhooks"
|
||||
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
||||
server_id
|
||||
)
|
||||
page_data[
|
||||
"user_permissions"
|
||||
] = self.controller.server_perms.get_user_id_permissions_list(
|
||||
exec_user["user_id"], server_id
|
||||
)
|
||||
page_data["permissions"] = {
|
||||
"Commands": EnumPermissionsServer.COMMANDS,
|
||||
"Terminal": EnumPermissionsServer.TERMINAL,
|
||||
"Logs": EnumPermissionsServer.LOGS,
|
||||
"Schedule": EnumPermissionsServer.SCHEDULE,
|
||||
"Backup": EnumPermissionsServer.BACKUP,
|
||||
"Files": EnumPermissionsServer.FILES,
|
||||
"Config": EnumPermissionsServer.CONFIG,
|
||||
"Players": EnumPermissionsServer.PLAYERS,
|
||||
}
|
||||
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
|
||||
server_id
|
||||
)
|
||||
page_data["server_stats"][
|
||||
"server_type"
|
||||
] = self.controller.servers.get_server_type_by_id(server_id)
|
||||
page_data["new_webhook"] = False
|
||||
page_data["webhook"] = self.controller.management.get_webhook_by_id(
|
||||
webhook_id
|
||||
)
|
||||
page_data["webhook"]["trigger"] = str(
|
||||
page_data["webhook"]["trigger"]
|
||||
).split(",")
|
||||
|
||||
page_data["providers"] = WebhookFactory.get_supported_providers()
|
||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
||||
|
||||
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
||||
if not superuser:
|
||||
self.redirect("/panel/error?error=Unauthorized access To Webhooks")
|
||||
return
|
||||
|
||||
template = "panel/server_webhook_edit.html"
|
||||
|
||||
elif page == "add_schedule":
|
||||
server_id = self.get_argument("id", None)
|
||||
if server_id is None:
|
||||
@ -1284,7 +1415,7 @@ class PanelHandler(BaseHandler):
|
||||
template = "panel/panel_edit_user_apikeys.html"
|
||||
|
||||
elif page == "remove_user":
|
||||
user_id = bleach.clean(self.get_argument("id", None))
|
||||
user_id = nh3.clean(self.get_argument("id", None))
|
||||
|
||||
if (
|
||||
not superuser
|
||||
@ -1490,7 +1621,8 @@ class PanelHandler(BaseHandler):
|
||||
logs_thread.start()
|
||||
self.redirect("/panel/dashboard")
|
||||
return
|
||||
|
||||
if self.helper.crafty_starting:
|
||||
template = "panel/loading.html"
|
||||
self.render(
|
||||
template,
|
||||
data=page_data,
|
||||
|
@ -1,11 +1,12 @@
|
||||
import logging
|
||||
import bleach
|
||||
import nh3
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
auth_log = logging.getLogger("auth")
|
||||
|
||||
|
||||
class PublicHandler(BaseHandler):
|
||||
@ -28,8 +29,8 @@ class PublicHandler(BaseHandler):
|
||||
# self.clear_cookie("user_data")
|
||||
|
||||
def get(self, page=None):
|
||||
error = bleach.clean(self.get_argument("error", "Invalid Login!"))
|
||||
error_msg = bleach.clean(self.get_argument("error_msg", ""))
|
||||
error = nh3.clean(self.get_argument("error", "Invalid Login!"))
|
||||
error_msg = nh3.clean(self.get_argument("error_msg", ""))
|
||||
|
||||
page_data = {
|
||||
"version": self.helper.get_version_string(),
|
||||
@ -82,8 +83,8 @@ class PublicHandler(BaseHandler):
|
||||
)
|
||||
|
||||
def post(self, page=None):
|
||||
error = bleach.clean(self.get_argument("error", "Invalid Login!"))
|
||||
error_msg = bleach.clean(self.get_argument("error_msg", ""))
|
||||
error = nh3.clean(self.get_argument("error", "Invalid Login!"))
|
||||
error_msg = nh3.clean(self.get_argument("error_msg", ""))
|
||||
|
||||
page_data = {
|
||||
"version": self.helper.get_version_string(),
|
||||
@ -96,18 +97,27 @@ class PublicHandler(BaseHandler):
|
||||
page_data["query"] = self.request.query
|
||||
|
||||
if page == "login":
|
||||
auth_log.info(
|
||||
f"User attempting to authenticate from {self.get_remote_ip()}"
|
||||
)
|
||||
next_page = "/login"
|
||||
if self.request.query:
|
||||
next_page = "/login?" + self.request.query
|
||||
|
||||
entered_username = bleach.clean(self.get_argument("username"))
|
||||
entered_password = bleach.clean(self.get_argument("password"))
|
||||
entered_username = nh3.clean(self.get_argument("username"))
|
||||
entered_password = self.get_argument("password")
|
||||
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
user_id = HelperUsers.get_user_id_by_name(entered_username.lower())
|
||||
user_data = HelperUsers.get_user_model(user_id)
|
||||
except:
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}."
|
||||
f" Authentication failed from remote IP {self.get_remote_ip()}"
|
||||
" Users does not exist."
|
||||
)
|
||||
error_msg = "Incorrect username or password. Please try again."
|
||||
# self.clear_cookie("user")
|
||||
# self.clear_cookie("user_data")
|
||||
@ -120,6 +130,12 @@ class PublicHandler(BaseHandler):
|
||||
|
||||
# if we don't have a user
|
||||
if not user_data:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}. Authentication"
|
||||
f" failed from remote IP {self.get_remote_ip()}"
|
||||
" User does not exist."
|
||||
)
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
error_msg = "Incorrect username or password. Please try again."
|
||||
# self.clear_cookie("user")
|
||||
# self.clear_cookie("user_data")
|
||||
@ -132,6 +148,12 @@ class PublicHandler(BaseHandler):
|
||||
|
||||
# if they are disabled
|
||||
if not user_data.enabled:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}. "
|
||||
f"Authentication failed from remote IP {self.get_remote_ip()}."
|
||||
" User account disabled"
|
||||
)
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
error_msg = (
|
||||
"User account disabled. Please contact "
|
||||
"your system administrator for more info."
|
||||
@ -159,7 +181,11 @@ class PublicHandler(BaseHandler):
|
||||
user_data.last_ip = self.get_remote_ip()
|
||||
user_data.last_login = Helpers.get_time_as_string()
|
||||
user_data.save()
|
||||
|
||||
auth_log.info(
|
||||
f"{entered_username} successfully"
|
||||
" authenticated and logged"
|
||||
f" into panel from remote IP {self.get_remote_ip()}"
|
||||
)
|
||||
# log this login
|
||||
self.controller.management.add_to_audit_log(
|
||||
user_data.user_id, "Logged in", 0, self.get_remote_ip()
|
||||
@ -172,6 +198,11 @@ class PublicHandler(BaseHandler):
|
||||
|
||||
self.redirect(next_page)
|
||||
else:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}."
|
||||
f" Authentication failed from remote IP {self.get_remote_ip()}"
|
||||
)
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
# self.clear_cookie("user")
|
||||
# self.clear_cookie("user_data")
|
||||
self.clear_cookie("token")
|
||||
|
@ -12,6 +12,7 @@ from app.classes.web.routes.api.roles.index import ApiRolesIndexHandler
|
||||
from app.classes.web.routes.api.roles.role.index import ApiRolesRoleIndexHandler
|
||||
from app.classes.web.routes.api.roles.role.servers import ApiRolesRoleServersHandler
|
||||
from app.classes.web.routes.api.roles.role.users import ApiRolesRoleUsersHandler
|
||||
|
||||
from app.classes.web.routes.api.servers.index import ApiServersIndexHandler
|
||||
from app.classes.web.routes.api.servers.server.action import (
|
||||
ApiServersServerActionHandler,
|
||||
@ -21,7 +22,13 @@ from app.classes.web.routes.api.servers.server.logs import ApiServersServerLogsH
|
||||
from app.classes.web.routes.api.servers.server.public import (
|
||||
ApiServersServerPublicHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.servers.server.status import (
|
||||
ApiServersServerStatusHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.servers.server.stats import ApiServersServerStatsHandler
|
||||
from app.classes.web.routes.api.servers.server.history import (
|
||||
ApiServersServerHistoryHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdinHandler
|
||||
from app.classes.web.routes.api.servers.server.tasks.index import (
|
||||
ApiServersServerTasksIndexHandler,
|
||||
@ -43,6 +50,12 @@ from app.classes.web.routes.api.servers.server.tasks.task.children import (
|
||||
from app.classes.web.routes.api.servers.server.tasks.task.index import (
|
||||
ApiServersServerTasksTaskIndexHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.servers.server.webhooks.index import (
|
||||
ApiServersServerWebhooksIndexHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.servers.server.webhooks.webhook.index import (
|
||||
ApiServersServerWebhooksManagementIndexHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
|
||||
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
|
||||
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
|
||||
@ -62,6 +75,7 @@ from app.classes.web.routes.api.crafty.config.index import (
|
||||
from app.classes.web.routes.api.crafty.config.server_dir import (
|
||||
ApiCraftyConfigServerDirHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.crafty.stats.stats import ApiCraftyHostStatsHandler
|
||||
from app.classes.web.routes.api.crafty.clogs.index import ApiCraftyLogIndexHandler
|
||||
from app.classes.web.routes.api.crafty.imports.index import ApiImportFilesIndexHandler
|
||||
from app.classes.web.routes.api.crafty.exe_cache import (
|
||||
@ -103,11 +117,21 @@ def api_handlers(handler_args):
|
||||
ApiCraftyConfigServerDirHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/crafty/stats/?",
|
||||
ApiCraftyHostStatsHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/crafty/logs/([a-z0-9_]+)/?",
|
||||
ApiCraftyLogIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/crafty/JarCache/?",
|
||||
ApiCraftyJarCacheIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/import/file/unzip/?",
|
||||
ApiImportFilesIndexHandler,
|
||||
@ -185,6 +209,11 @@ def api_handlers(handler_args):
|
||||
ApiCraftySteamCacheIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/servers/status/?",
|
||||
ApiServersServerStatusHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/servers/([0-9]+)/?",
|
||||
ApiServersServerIndexHandler,
|
||||
@ -235,6 +264,21 @@ def api_handlers(handler_args):
|
||||
ApiServersServerStatsHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/servers/([0-9]+)/history/?",
|
||||
ApiServersServerHistoryHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/servers/([0-9]+)/webhook/([0-9]+)/?",
|
||||
ApiServersServerWebhooksManagementIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/servers/([0-9]+)/webhook/?",
|
||||
ApiServersServerWebhooksIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/servers/([0-9]+)/action/([a-z_]+)/?",
|
||||
ApiServersServerActionHandler,
|
||||
|
@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
auth_log = logging.getLogger("auth")
|
||||
login_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
try:
|
||||
data = json.loads(self.request.body)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Invalid JSON schema for API"
|
||||
f" login attempt from {self.get_remote_ip()}"
|
||||
)
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||
)
|
||||
@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
try:
|
||||
validate(data, login_schema)
|
||||
except ValidationError as e:
|
||||
logger.error(
|
||||
"Invalid JSON schema for API"
|
||||
f" login attempt from {self.get_remote_ip()}"
|
||||
)
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
user_data = Users.get_or_none(Users.username == username)
|
||||
|
||||
if user_data is None:
|
||||
self.controller.log_attempt(self.get_remote_ip(), username)
|
||||
auth_log.error(
|
||||
f"User attempted to log into {username}."
|
||||
" Authentication failed from remote IP"
|
||||
f" {self.get_remote_ip()}. User not found"
|
||||
)
|
||||
return self.finish_json(
|
||||
401,
|
||||
{"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None},
|
||||
)
|
||||
|
||||
if not user_data.enabled:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {username}."
|
||||
" Authentication failed from remote"
|
||||
f" IP {self.get_remote_ip()} account disabled"
|
||||
)
|
||||
self.finish_json(
|
||||
403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None}
|
||||
)
|
||||
@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
|
||||
# Valid Login
|
||||
if login_result:
|
||||
auth_log.info(
|
||||
f"{username} successfully"
|
||||
" authenticated and logged"
|
||||
f" into panel from remote IP {self.get_remote_ip()}"
|
||||
)
|
||||
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
|
||||
|
||||
# record this login
|
||||
|
@ -29,6 +29,14 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
|
||||
) = auth_data
|
||||
|
||||
data = self.helper.get_announcements()
|
||||
if not data:
|
||||
return self.finish_json(
|
||||
424,
|
||||
{
|
||||
"status": "error",
|
||||
"data": "Failed to get announcements",
|
||||
},
|
||||
)
|
||||
cleared = str(
|
||||
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
|
||||
"cleared_notifs"
|
||||
@ -84,6 +92,14 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
|
||||
},
|
||||
)
|
||||
announcements = self.helper.get_announcements()
|
||||
if not announcements:
|
||||
return self.finish_json(
|
||||
424,
|
||||
{
|
||||
"status": "error",
|
||||
"data": "Failed to get current announcements",
|
||||
},
|
||||
)
|
||||
res = [d.get("id", None) for d in announcements]
|
||||
cleared_notifs = str(
|
||||
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
|
||||
|
@ -7,6 +7,7 @@ from jsonschema.exceptions import ValidationError
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.web.websocket_handler import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
files_get_schema = {
|
||||
@ -64,14 +65,16 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
|
||||
# JSON we need to remove this and just send
|
||||
# the path.
|
||||
if data["upload"]:
|
||||
folder = os.path.join(self.controller.project_root, "imports", folder)
|
||||
folder = os.path.join(
|
||||
self.controller.project_root, "import", "upload", folder
|
||||
)
|
||||
if Helpers.check_file_exists(folder):
|
||||
folder = self.file_helper.unzip_server(folder, user_id)
|
||||
root_path = True
|
||||
else:
|
||||
if user_id:
|
||||
user_lang = self.controller.users.get_user_lang_by_id(user_id)
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
@ -83,7 +86,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
|
||||
else:
|
||||
if not self.helper.check_path_exists(folder) and user_id:
|
||||
user_lang = self.controller.users.get_user_lang_by_id(user_id)
|
||||
self.helper.websocket_helper.broadcast_user(
|
||||
WebSocketManager().broadcast_user(
|
||||
user_id,
|
||||
"send_start_error",
|
||||
{
|
||||
|
21
app/classes/web/routes/api/crafty/stats/stats.py
Normal file
@ -0,0 +1,21 @@
|
||||
import logging
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiCraftyHostStatsHandler(BaseApiHandler):
|
||||
def get(self):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
latest = self.controller.management.get_latest_hosts_stats()
|
||||
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": latest,
|
||||
},
|
||||
)
|
@ -110,7 +110,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
||||
|
||||
try:
|
||||
data = orjson.loads(self.request.body)
|
||||
except orjson.decoder.JSONDecodeError as e:
|
||||
except orjson.JSONDecodeError as e:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
new_server_schema = {
|
||||
"definitions": {},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "Root",
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -72,9 +72,9 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
||||
FileHelpers.del_file(
|
||||
os.path.join(backup_conf["backup_path"], data["filename"])
|
||||
)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "NO BACKUP FOUND"}
|
||||
400, {"status": "error", "error": f"DELETE FAILED with error {e}"}
|
||||
)
|
||||
self.controller.management.add_to_audit_log(
|
||||
auth_data[4]["user_id"],
|
||||
@ -121,11 +121,11 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
||||
server_data = self.controller.servers.get_server_data_by_id(server_id)
|
||||
zip_name = data["filename"]
|
||||
# import the server again based on zipfile
|
||||
if server_data["type"] == "minecraft-java":
|
||||
backup_path = svr_obj.backup_path
|
||||
if Helpers.validate_traversal(backup_path, zip_name):
|
||||
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
|
||||
new_server = self.controller.import_zip_server(
|
||||
backup_path = svr_obj.backup_path
|
||||
if Helpers.validate_traversal(backup_path, zip_name):
|
||||
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
|
||||
if server_data["type"] == "minecraft-java":
|
||||
new_server = self.controller.restore_java_zip_server(
|
||||
svr_obj.server_name,
|
||||
temp_dir,
|
||||
server_data["executable"],
|
||||
@ -134,71 +134,78 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
||||
server_data["server_port"],
|
||||
server_data["created_by"],
|
||||
)
|
||||
new_server_id = new_server
|
||||
new_server = self.controller.servers.get_server_data(new_server)
|
||||
self.controller.rename_backup_dir(
|
||||
server_id, new_server_id, new_server["server_uuid"]
|
||||
elif server_data["type"] == "minecraft-bedrock":
|
||||
new_server = self.controller.restore_bedrock_zip_server(
|
||||
svr_obj.server_name,
|
||||
temp_dir,
|
||||
server_data["executable"],
|
||||
server_data["server_port"],
|
||||
server_data["created_by"],
|
||||
)
|
||||
# preserve current schedules
|
||||
for schedule in self.controller.management.get_schedules_by_server(
|
||||
server_id
|
||||
):
|
||||
self.tasks_manager.update_job(
|
||||
schedule.schedule_id, {"server_id": new_server_id}
|
||||
)
|
||||
# preserve execution command
|
||||
new_server_obj = self.controller.servers.get_server_obj(
|
||||
new_server_id
|
||||
new_server_id = new_server
|
||||
new_server = self.controller.servers.get_server_data(new_server)
|
||||
self.controller.rename_backup_dir(
|
||||
server_id, new_server_id, new_server["server_uuid"]
|
||||
)
|
||||
# preserve current schedules
|
||||
for schedule in self.controller.management.get_schedules_by_server(
|
||||
server_id
|
||||
):
|
||||
job_data = self.controller.management.get_scheduled_task(
|
||||
schedule.schedule_id
|
||||
)
|
||||
new_server_obj.execution_command = server_data["execution_command"]
|
||||
# reset executable path
|
||||
if svr_obj.path in svr_obj.executable:
|
||||
new_server_obj.executable = str(svr_obj.executable).replace(
|
||||
svr_obj.path, new_server_obj.path
|
||||
)
|
||||
# reset run command path
|
||||
if svr_obj.path in svr_obj.execution_command:
|
||||
new_server_obj.execution_command = str(
|
||||
svr_obj.execution_command
|
||||
).replace(svr_obj.path, new_server_obj.path)
|
||||
# reset log path
|
||||
if svr_obj.path in svr_obj.log_path:
|
||||
new_server_obj.log_path = str(svr_obj.log_path).replace(
|
||||
svr_obj.path, new_server_obj.path
|
||||
)
|
||||
self.controller.servers.update_server(new_server_obj)
|
||||
job_data["server_id"] = new_server_id
|
||||
del job_data["schedule_id"]
|
||||
self.tasks_manager.update_job(schedule.schedule_id, job_data)
|
||||
# preserve execution command
|
||||
new_server_obj = self.controller.servers.get_server_obj(new_server_id)
|
||||
new_server_obj.execution_command = server_data["execution_command"]
|
||||
# reset executable path
|
||||
if svr_obj.path in svr_obj.executable:
|
||||
new_server_obj.executable = str(svr_obj.executable).replace(
|
||||
svr_obj.path, new_server_obj.path
|
||||
)
|
||||
# reset run command path
|
||||
if svr_obj.path in svr_obj.execution_command:
|
||||
new_server_obj.execution_command = str(
|
||||
svr_obj.execution_command
|
||||
).replace(svr_obj.path, new_server_obj.path)
|
||||
# reset log path
|
||||
if svr_obj.path in svr_obj.log_path:
|
||||
new_server_obj.log_path = str(svr_obj.log_path).replace(
|
||||
svr_obj.path, new_server_obj.path
|
||||
)
|
||||
self.controller.servers.update_server(new_server_obj)
|
||||
|
||||
# preserve backup config
|
||||
backup_config = self.controller.management.get_backup_config(
|
||||
server_id
|
||||
)
|
||||
excluded_dirs = []
|
||||
server_obj = self.controller.servers.get_server_obj(server_id)
|
||||
loop_backup_path = self.helper.wtol_path(server_obj.path)
|
||||
for item in self.controller.management.get_excluded_backup_dirs(
|
||||
server_id
|
||||
):
|
||||
item_path = self.helper.wtol_path(item)
|
||||
bu_path = os.path.relpath(item_path, loop_backup_path)
|
||||
bu_path = os.path.join(new_server_obj.path, bu_path)
|
||||
excluded_dirs.append(bu_path)
|
||||
self.controller.management.set_backup_config(
|
||||
new_server_id,
|
||||
new_server_obj.backup_path,
|
||||
backup_config["max_backups"],
|
||||
excluded_dirs,
|
||||
backup_config["compress"],
|
||||
backup_config["shutdown"],
|
||||
)
|
||||
# remove old server's tasks
|
||||
try:
|
||||
self.tasks_manager.remove_all_server_tasks(server_id)
|
||||
except JobLookupError as e:
|
||||
logger.info("No active tasks found for server: {e}")
|
||||
self.controller.remove_server(server_id, True)
|
||||
except Exception:
|
||||
# preserve backup config
|
||||
backup_config = self.controller.management.get_backup_config(server_id)
|
||||
excluded_dirs = []
|
||||
server_obj = self.controller.servers.get_server_obj(server_id)
|
||||
loop_backup_path = self.helper.wtol_path(server_obj.path)
|
||||
for item in self.controller.management.get_excluded_backup_dirs(
|
||||
server_id
|
||||
):
|
||||
item_path = self.helper.wtol_path(item)
|
||||
bu_path = os.path.relpath(item_path, loop_backup_path)
|
||||
bu_path = os.path.join(new_server_obj.path, bu_path)
|
||||
excluded_dirs.append(bu_path)
|
||||
self.controller.management.set_backup_config(
|
||||
new_server_id,
|
||||
new_server_obj.backup_path,
|
||||
backup_config["max_backups"],
|
||||
excluded_dirs,
|
||||
backup_config["compress"],
|
||||
backup_config["shutdown"],
|
||||
)
|
||||
# remove old server's tasks
|
||||
try:
|
||||
self.tasks_manager.remove_all_server_tasks(server_id)
|
||||
except JobLookupError as e:
|
||||
logger.info("No active tasks found for server: {e}")
|
||||
self.controller.remove_server(server_id, True)
|
||||
except Exception as e:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "NO BACKUP FOUND"}
|
||||
400, {"status": "error", "error": f"NO BACKUP FOUND {e}"}
|
||||
)
|
||||
self.controller.management.add_to_audit_log(
|
||||
auth_data[4]["user_id"],
|
||||
|
@ -86,7 +86,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
or EnumPermissionsServer.BACKUP
|
||||
and EnumPermissionsServer.BACKUP
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
|
28
app/classes/web/routes/api/servers/server/history.py
Normal file
@ -0,0 +1,28 @@
|
||||
import logging
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.controllers.servers_controller import ServersController
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiServersServerHistoryHandler(BaseApiHandler):
|
||||
def get(self, server_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||
# if the user doesn't have access to the server, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
srv = ServersController().get_server_instance_by_id(server_id)
|
||||
history = srv.get_server_history()
|
||||
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": history,
|
||||
},
|
||||
)
|
32
app/classes/web/routes/api/servers/server/status.py
Normal file
@ -0,0 +1,32 @@
|
||||
import logging
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiServersServerStatusHandler(BaseApiHandler):
|
||||
def get(self):
|
||||
servers_status = []
|
||||
servers_list = self.controller.servers.get_all_servers_stats()
|
||||
for server in servers_list:
|
||||
if server.get("server_data").get("show_status") is True:
|
||||
servers_status.append(
|
||||
{
|
||||
"id": server.get("server_data").get("server_id"),
|
||||
"world_name": server.get("stats").get("world_name"),
|
||||
"running": server.get("stats").get("running"),
|
||||
"online": server.get("stats").get("online"),
|
||||
"max": server.get("stats").get("max"),
|
||||
"version": server.get("stats").get("version"),
|
||||
"desc": server.get("stats").get("desc"),
|
||||
"icon": server.get("stats").get("icon"),
|
||||
}
|
||||
)
|
||||
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": servers_status,
|
||||
},
|
||||
)
|
108
app/classes/web/routes/api/servers/server/webhooks/index.py
Normal file
@ -0,0 +1,108 @@
|
||||
# TODO: create and read
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from jsonschema import ValidationError, validate
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
new_webhook_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"webhook_type": {
|
||||
"type": "string",
|
||||
"enum": WebhookFactory.get_supported_providers(),
|
||||
},
|
||||
"name": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"bot_name": {"type": "string"},
|
||||
"trigger": {"type": "array"},
|
||||
"body": {"type": "string"},
|
||||
"color": {"type": "string", "default": "#005cd1"},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 7,
|
||||
}
|
||||
|
||||
|
||||
class ApiServersServerWebhooksIndexHandler(BaseApiHandler):
|
||||
def get(self, server_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
if (
|
||||
EnumPermissionsServer.CONFIG
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
):
|
||||
# if the user doesn't have Schedule permission, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": self.controller.management.get_webhooks_by_server(server_id),
|
||||
},
|
||||
)
|
||||
|
||||
def post(self, server_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(self.request.body)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||
)
|
||||
|
||||
try:
|
||||
validate(data, new_webhook_schema)
|
||||
except ValidationError as e:
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "INVALID_JSON_SCHEMA",
|
||||
"error_data": str(e),
|
||||
},
|
||||
)
|
||||
|
||||
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||
# if the user doesn't have access to the server, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
if (
|
||||
EnumPermissionsServer.CONFIG
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
):
|
||||
# if the user doesn't have Schedule permission, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
data["server_id"] = server_id
|
||||
|
||||
self.controller.management.add_to_audit_log(
|
||||
auth_data[4]["user_id"],
|
||||
f"Edited server {server_id}: added webhook",
|
||||
server_id,
|
||||
self.get_remote_ip(),
|
||||
)
|
||||
triggers = ""
|
||||
for item in data["trigger"]:
|
||||
string = item + ","
|
||||
triggers += string
|
||||
data["trigger"] = triggers
|
||||
webhook_id = self.controller.management.create_webhook(data)
|
||||
|
||||
self.finish_json(200, {"status": "ok", "data": {"webhook_id": webhook_id}})
|
@ -0,0 +1,187 @@
|
||||
# TODO: read and delete
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from jsonschema import ValidationError, validate
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
webhook_patch_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"webhook_type": {
|
||||
"type": "string",
|
||||
"enum": WebhookFactory.get_supported_providers(),
|
||||
},
|
||||
"name": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"bot_name": {"type": "string"},
|
||||
"trigger": {"type": "array"},
|
||||
"body": {"type": "string"},
|
||||
"color": {"type": "string", "default": "#005cd1"},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
}
|
||||
|
||||
|
||||
class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
|
||||
def get(self, server_id: str, webhook_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
if (
|
||||
EnumPermissionsServer.CONFIG
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
):
|
||||
# if the user doesn't have Schedule permission, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
if (
|
||||
not str(webhook_id)
|
||||
in self.controller.management.get_webhooks_by_server(server_id).keys()
|
||||
):
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "NO WEBHOOK FOUND"}
|
||||
)
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": self.controller.management.get_webhook_by_id(webhook_id),
|
||||
},
|
||||
)
|
||||
|
||||
def delete(self, server_id: str, webhook_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
if (
|
||||
EnumPermissionsServer.CONFIG
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
):
|
||||
# if the user doesn't have Schedule permission, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
try:
|
||||
self.controller.management.delete_webhook(webhook_id)
|
||||
except Exception:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "NO WEBHOOK FOUND"}
|
||||
)
|
||||
self.controller.management.add_to_audit_log(
|
||||
auth_data[4]["user_id"],
|
||||
f"Edited server {server_id}: removed webhook",
|
||||
server_id,
|
||||
self.get_remote_ip(),
|
||||
)
|
||||
|
||||
return self.finish_json(200, {"status": "ok"})
|
||||
|
||||
def patch(self, server_id: str, webhook_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(self.request.body)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||
)
|
||||
|
||||
try:
|
||||
validate(data, webhook_patch_schema)
|
||||
except ValidationError as e:
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "INVALID_JSON_SCHEMA",
|
||||
"error_data": str(e),
|
||||
},
|
||||
)
|
||||
|
||||
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||
# if the user doesn't have access to the server, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
if (
|
||||
EnumPermissionsServer.CONFIG
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
):
|
||||
# if the user doesn't have Schedule permission, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
data["server_id"] = server_id
|
||||
if "trigger" in data.keys():
|
||||
triggers = ""
|
||||
for item in data["trigger"]:
|
||||
string = item + ","
|
||||
triggers += string
|
||||
data["trigger"] = triggers
|
||||
self.controller.management.modify_webhook(webhook_id, data)
|
||||
|
||||
self.controller.management.add_to_audit_log(
|
||||
auth_data[4]["user_id"],
|
||||
f"Edited server {server_id}: updated webhook",
|
||||
server_id,
|
||||
self.get_remote_ip(),
|
||||
)
|
||||
|
||||
self.finish_json(200, {"status": "ok"})
|
||||
|
||||
def post(self, server_id: str, webhook_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
self.controller.management.add_to_audit_log(
|
||||
auth_data[4]["user_id"],
|
||||
"Tested webhook",
|
||||
server_id,
|
||||
self.get_remote_ip(),
|
||||
)
|
||||
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||
# if the user doesn't have access to the server, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
if (
|
||||
EnumPermissionsServer.CONFIG
|
||||
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||
auth_data[4]["user_id"], server_id
|
||||
)
|
||||
):
|
||||
# if the user doesn't have Schedule permission, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
webhook = self.controller.management.get_webhook_by_id(webhook_id)
|
||||
try:
|
||||
webhook_provider = WebhookFactory.create_provider(webhook["webhook_type"])
|
||||
webhook_provider.send(
|
||||
server_name=self.controller.servers.get_server_data_by_id(server_id)[
|
||||
"server_name"
|
||||
],
|
||||
title=f"Test Webhook: {webhook['name']}",
|
||||
url=webhook["url"],
|
||||
message=webhook["body"],
|
||||
color=webhook["color"], # Prestigious purple!
|
||||
bot_name="Crafty Webhooks Tester",
|
||||
)
|
||||
except Exception as e:
|
||||
self.finish_json(500, {"status": "error", "error": str(e)})
|
||||
|
||||
self.finish_json(200, {"status": "ok"})
|
@ -4,10 +4,7 @@ import typing as t
|
||||
|
||||
from jsonschema import ValidationError, validate
|
||||
from app.classes.controllers.users_controller import UsersController
|
||||
from app.classes.models.crafty_permissions import (
|
||||
EnumPermissionsCrafty,
|
||||
PermissionsCrafty,
|
||||
)
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
from app.classes.models.roles import HelperRoles
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
@ -218,7 +215,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
||||
|
||||
user_obj = HelperUsers.get_user_model(user_id)
|
||||
if "password" in data and str(user["user_id"]) != str(user_id):
|
||||
if str(user["user_id"]) != str(user_obj.manager):
|
||||
if str(user["user_id"]) != str(user_obj.manager) and not user["superuser"]:
|
||||
# TODO: edit your own password
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
|
||||
@ -247,31 +244,25 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
||||
or data["manager"] == 0
|
||||
):
|
||||
data["manager"] = None
|
||||
|
||||
crafty_perms = None
|
||||
if "permissions" in data:
|
||||
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
|
||||
"permissions"
|
||||
)
|
||||
permissions_mask = "0" * len(EnumPermissionsCrafty)
|
||||
limit_server_creation = 0
|
||||
limit_user_creation = 0
|
||||
limit_role_creation = 0
|
||||
|
||||
for permission in permissions:
|
||||
permissions_mask = self.controller.crafty_perms.set_permission(
|
||||
permissions_mask,
|
||||
EnumPermissionsCrafty.__members__[permission["name"]],
|
||||
"1" if permission["enabled"] else "0",
|
||||
)
|
||||
|
||||
PermissionsCrafty.add_or_update_user(
|
||||
user_id,
|
||||
permissions_mask,
|
||||
limit_server_creation,
|
||||
limit_user_creation,
|
||||
limit_role_creation,
|
||||
)
|
||||
|
||||
if permissions is not None:
|
||||
server_quantity = {}
|
||||
permissions_mask = list(permissions_mask)
|
||||
for permission in permissions:
|
||||
server_quantity[permission["name"]] = permission["quantity"]
|
||||
permissions_mask[
|
||||
EnumPermissionsCrafty[permission["name"]].value
|
||||
] = ("1" if permission["enabled"] else "0")
|
||||
permissions_mask = "".join(permissions_mask)
|
||||
crafty_perms = {
|
||||
"permissions_mask": permissions_mask,
|
||||
"server_quantity": server_quantity,
|
||||
}
|
||||
# TODO: make this more efficient
|
||||
if len(data) != 0:
|
||||
for key in data:
|
||||
@ -280,7 +271,11 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
||||
if key == "password":
|
||||
value = self.helper.encode_pass(value)
|
||||
setattr(user_obj, key, value)
|
||||
user_obj.save()
|
||||
self.controller.users.update_user(
|
||||
user_id,
|
||||
data,
|
||||
crafty_perms,
|
||||
)
|
||||
|
||||
self.controller.management.add_to_audit_log(
|
||||
user["user_id"],
|
||||
|
31
app/classes/web/routes/metrics/host.py
Normal file
@ -0,0 +1,31 @@
|
||||
from prometheus_client.exposition import _bake_output
|
||||
from prometheus_client.exposition import parse_qs, urlparse
|
||||
|
||||
from app.classes.web.metrics_handler import BaseMetricsHandler
|
||||
|
||||
|
||||
# Decorate function with metric.
|
||||
class ApiOpenMetricsCraftyHandler(BaseMetricsHandler):
|
||||
def get(self):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
if not auth_data[3]:
|
||||
# if the user doesn't have access to the server, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
self.get_registry()
|
||||
|
||||
def get_registry(self) -> None:
|
||||
# Prepare parameters
|
||||
registry = self.controller.management.host_registry
|
||||
accept_header = self.request.headers.get("Accept")
|
||||
accept_encoding_header = self.request.headers.get("Accept-Encoding")
|
||||
params = parse_qs(urlparse(self.request.path).query)
|
||||
# Bake output
|
||||
status, headers, output = _bake_output(
|
||||
registry, accept_header, accept_encoding_header, params, False
|
||||
)
|
||||
# Return output
|
||||
self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output)
|
21
app/classes/web/routes/metrics/index.py
Normal file
@ -0,0 +1,21 @@
|
||||
from prometheus_client import Info
|
||||
from app.classes.web.metrics_handler import BaseMetricsHandler
|
||||
|
||||
CRAFTY_INFO = Info("Crafty_Controller", "Infos of this Crafty Instance")
|
||||
|
||||
|
||||
# Decorate function with metric.
|
||||
class ApiOpenMetricsIndexHandler(BaseMetricsHandler):
|
||||
def get(self):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
version = f"{self.helper.get_version().get('major')} \
|
||||
.{self.helper.get_version().get('minor')} \
|
||||
.{self.helper.get_version().get('sub')}"
|
||||
CRAFTY_INFO.info(
|
||||
{"version": version, "docker": f"{self.helper.is_env_docker()}"}
|
||||
)
|
||||
|
||||
self.get_registry()
|
24
app/classes/web/routes/metrics/metrics_handlers.py
Normal file
@ -0,0 +1,24 @@
|
||||
from app.classes.web.routes.metrics.index import ApiOpenMetricsIndexHandler
|
||||
from app.classes.web.routes.metrics.host import ApiOpenMetricsCraftyHandler
|
||||
from app.classes.web.routes.metrics.servers import ApiOpenMetricsServersHandler
|
||||
|
||||
|
||||
def metrics_handlers(handler_args):
|
||||
return [
|
||||
# OpenMetrics routes
|
||||
(
|
||||
r"/metrics/?",
|
||||
ApiOpenMetricsIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/metrics/host/?",
|
||||
ApiOpenMetricsCraftyHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/metrics/servers/([0-9]+)/?",
|
||||
ApiOpenMetricsServersHandler,
|
||||
handler_args,
|
||||
),
|
||||
]
|
37
app/classes/web/routes/metrics/servers.py
Normal file
@ -0,0 +1,37 @@
|
||||
from prometheus_client.exposition import _bake_output
|
||||
from prometheus_client.exposition import parse_qs, urlparse
|
||||
|
||||
from app.classes.web.metrics_handler import BaseMetricsHandler
|
||||
from app.classes.controllers.servers_controller import ServersController
|
||||
|
||||
|
||||
# Decorate function with metric.
|
||||
class ApiOpenMetricsServersHandler(BaseMetricsHandler):
|
||||
def get(self, server_id: str):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
|
||||
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||
# if the user doesn't have access to the server, return an error
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
|
||||
self.get_registry(server_id)
|
||||
|
||||
def get_registry(self, server_id=None) -> None:
|
||||
if server_id is None:
|
||||
return self.finish_json(500, {"status": "error", "error": "UNKNOWN_SERVER"})
|
||||
|
||||
# Prepare parameters
|
||||
registry = (
|
||||
ServersController().get_server_instance_by_id(server_id).server_registry
|
||||
)
|
||||
accept_header = self.request.headers.get("Accept")
|
||||
accept_encoding_header = self.request.headers.get("Accept-Encoding")
|
||||
params = parse_qs(urlparse(self.request.path).query)
|
||||
# Bake output
|
||||
status, headers, output = _bake_output(
|
||||
registry, accept_header, accept_encoding_header, params, False
|
||||
)
|
||||
# Return output
|
||||
self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output)
|
@ -14,25 +14,15 @@ import tornado.httpserver
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.web.public_handler import PublicHandler
|
||||
from app.classes.web.panel_handler import PanelHandler
|
||||
from app.classes.web.default_handler import DefaultHandler
|
||||
from app.classes.web.routes.api.api_handlers import api_handlers
|
||||
from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers
|
||||
from app.classes.web.server_handler import ServerHandler
|
||||
from app.classes.web.api_handler import (
|
||||
ServersStats,
|
||||
NodeStats,
|
||||
ServerBackup,
|
||||
StartServer,
|
||||
StopServer,
|
||||
RestartServer,
|
||||
CreateUser,
|
||||
DeleteUser,
|
||||
ListServers,
|
||||
SendCommand,
|
||||
)
|
||||
from app.classes.web.websocket_handler import SocketHandler
|
||||
from app.classes.web.websocket_handler import WebSocketHandler
|
||||
from app.classes.web.static_handler import CustomStaticHandler
|
||||
from app.classes.web.upload_handler import UploadHandler
|
||||
from app.classes.web.http_handler import HTTPHandler, HTTPHandlerPage
|
||||
@ -46,7 +36,13 @@ class Webserver:
|
||||
controller: Controller
|
||||
helper: Helpers
|
||||
|
||||
def __init__(self, helper, controller, tasks_manager, file_helper):
|
||||
def __init__(
|
||||
self,
|
||||
helper: Helpers,
|
||||
controller: Controller,
|
||||
tasks_manager,
|
||||
file_helper: FileHelpers,
|
||||
):
|
||||
self.ioloop = None
|
||||
self.http_server = None
|
||||
self.https_server = None
|
||||
@ -151,22 +147,13 @@ class Webserver:
|
||||
(r"/", DefaultHandler, handler_args),
|
||||
(r"/panel/(.*)", PanelHandler, handler_args),
|
||||
(r"/server/(.*)", ServerHandler, handler_args),
|
||||
(r"/ws", SocketHandler, handler_args),
|
||||
(r"/ws", WebSocketHandler, handler_args),
|
||||
(r"/upload", UploadHandler, handler_args),
|
||||
(r"/status", StatusHandler, handler_args),
|
||||
# API Routes V1
|
||||
(r"/api/v1/stats/servers", ServersStats, handler_args),
|
||||
(r"/api/v1/stats/node", NodeStats, handler_args),
|
||||
(r"/api/v1/server/send_command", SendCommand, handler_args),
|
||||
(r"/api/v1/server/backup", ServerBackup, handler_args),
|
||||
(r"/api/v1/server/start", StartServer, handler_args),
|
||||
(r"/api/v1/server/stop", StopServer, handler_args),
|
||||
(r"/api/v1/server/restart", RestartServer, handler_args),
|
||||
(r"/api/v1/list_servers", ListServers, handler_args),
|
||||
(r"/api/v1/users/create_user", CreateUser, handler_args),
|
||||
(r"/api/v1/users/delete_user", DeleteUser, handler_args),
|
||||
# API Routes V2
|
||||
*api_handlers(handler_args),
|
||||
# API Routes OpenMetrics
|
||||
*metrics_handlers(handler_args),
|
||||
# Using this one at the end
|
||||
# to catch all the other requests to Public Handler
|
||||
(r"/(.*)", PublicHandler, handler_args),
|
||||
|
@ -12,6 +12,7 @@ from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -101,7 +102,8 @@ class UploadHandler(BaseHandler):
|
||||
)
|
||||
self.do_upload = False
|
||||
|
||||
path = os.path.join(self.controller.project_root, "imports")
|
||||
path = os.path.join(self.controller.project_root, "import", "upload")
|
||||
self.helper.ensure_dir_exists(path)
|
||||
# Delete existing files
|
||||
if len(os.listdir(path)) > 0:
|
||||
for item in os.listdir():
|
||||
@ -115,7 +117,7 @@ class UploadHandler(BaseHandler):
|
||||
self.request.headers.get("X-FileName", None)
|
||||
)
|
||||
if not str(filename).endswith(".zip"):
|
||||
self.helper.websocket_helper.broadcast("close_upload_box", "error")
|
||||
WebSocketManager().broadcast("close_upload_box", "error")
|
||||
self.finish("error")
|
||||
full_path = os.path.join(path, filename)
|
||||
|
||||
@ -315,13 +317,13 @@ class UploadHandler(BaseHandler):
|
||||
if self.do_upload:
|
||||
time.sleep(5)
|
||||
if files_left == 0:
|
||||
self.helper.websocket_helper.broadcast("close_upload_box", "success")
|
||||
WebSocketManager().broadcast("close_upload_box", "success")
|
||||
self.finish("success") # Nope, I'm sending "success"
|
||||
self.f.close()
|
||||
else:
|
||||
time.sleep(5)
|
||||
if files_left == 0:
|
||||
self.helper.websocket_helper.broadcast("close_upload_box", "error")
|
||||
WebSocketManager().broadcast("close_upload_box", "error")
|
||||
self.finish("error")
|
||||
|
||||
def data_received(self, chunk):
|
||||
|
39
app/classes/web/webhooks/base_webhook.py
Normal file
@ -0,0 +1,39 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
helper = Helpers()
|
||||
|
||||
|
||||
class WebhookProvider(ABC):
|
||||
"""
|
||||
Base class for all webhook providers.
|
||||
|
||||
Provides a common interface for all webhook provider implementations,
|
||||
ensuring that each provider will have a send method.
|
||||
"""
|
||||
|
||||
WEBHOOK_USERNAME = "Crafty Webhooks"
|
||||
WEBHOOK_PFP_URL = (
|
||||
"https://gitlab.com/crafty-controller/crafty-4/-"
|
||||
+ "/raw/master/app/frontend/static/assets/images/"
|
||||
+ "Crafty_4-0.png"
|
||||
)
|
||||
CRAFTY_VERSION = helper.get_version_string()
|
||||
|
||||
def _send_request(self, url, payload, headers=None):
|
||||
"""Send a POST request to the given URL with the provided payload."""
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return "Dispatch successful"
|
||||
except requests.RequestException as error:
|
||||
logger.error(error)
|
||||
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
|
||||
|
||||
@abstractmethod
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""Abstract method that derived classes will implement for sending webhooks."""
|
82
app/classes/web/webhooks/discord_webhook.py
Normal file
@ -0,0 +1,82 @@
|
||||
from datetime import datetime
|
||||
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||
|
||||
|
||||
class DiscordWebhook(WebhookProvider):
|
||||
def _construct_discord_payload(self, server_name, title, message, color, bot_name):
|
||||
"""
|
||||
Constructs the payload required for sending a Discord webhook notification.
|
||||
|
||||
This method prepares a payload for the Discord webhook API using the provided
|
||||
message content, the Crafty Controller version, and the current UTC datetime.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
message (str): The main content of the notification message.
|
||||
color (int): The color code for the side stripe in the Discord embed message.
|
||||
bot_name (str): Override for the Webhook's name set on creation
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||
|
||||
Note:
|
||||
- Discord embed designer
|
||||
- https://discohook.org/
|
||||
"""
|
||||
current_datetime = datetime.utcnow()
|
||||
formatted_datetime = (
|
||||
current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||
)
|
||||
|
||||
# Convert the hex to an integer
|
||||
sanitized_hex = color[1:] if color.startswith("#") else color
|
||||
color_int = int(sanitized_hex, 16)
|
||||
|
||||
headers = {"Content-type": "application/json"}
|
||||
payload = {
|
||||
"username": bot_name,
|
||||
"avatar_url": self.WEBHOOK_PFP_URL,
|
||||
"embeds": [
|
||||
{
|
||||
"title": title,
|
||||
"description": message,
|
||||
"color": color_int,
|
||||
"author": {"name": server_name},
|
||||
"footer": {"text": f"Crafty Controller v.{self.CRAFTY_VERSION}"},
|
||||
"timestamp": formatted_datetime,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return payload, headers
|
||||
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""
|
||||
Sends a Discord webhook notification using the given details.
|
||||
|
||||
The method constructs and dispatches a payload suitable for
|
||||
Discords's webhook system.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
url (str): The webhook URL to send the notification to.
|
||||
message (str): The main content or body of the notification message.
|
||||
color (str, optional): The color code for the embed's side stripe.
|
||||
Defaults to a pretty blue if not provided.
|
||||
bot_name (str): Override for the Webhook's name set on creation
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
"""
|
||||
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
|
||||
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||
payload, headers = self._construct_discord_payload(
|
||||
server_name, title, message, color, bot_name
|
||||
)
|
||||
return self._send_request(url, payload, headers)
|
74
app/classes/web/webhooks/mattermost_webhook.py
Normal file
@ -0,0 +1,74 @@
|
||||
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||
|
||||
|
||||
class MattermostWebhook(WebhookProvider):
|
||||
def _construct_mattermost_payload(self, server_name, title, message, bot_name):
|
||||
"""
|
||||
Constructs the payload required for sending a Mattermost webhook notification.
|
||||
|
||||
The method formats the given information into a Markdown-styled message for MM,
|
||||
including an information card containing the Crafty version.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
message (str): The main content of the notification message.
|
||||
bot_name (str): Override for the Webhook's name set on creation.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||
"""
|
||||
formatted_text = (
|
||||
f"-----\n\n"
|
||||
f"#### {title}\n"
|
||||
f"##### Server: ```{server_name}```\n\n"
|
||||
f"```\n{message}\n```\n\n"
|
||||
f"-----"
|
||||
)
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
payload = {
|
||||
"text": formatted_text,
|
||||
"username": bot_name,
|
||||
"icon_url": self.WEBHOOK_PFP_URL,
|
||||
"props": {
|
||||
"card": (
|
||||
f"[Crafty Controller "
|
||||
f"v.{self.CRAFTY_VERSION}](https://craftycontrol.com)"
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
return payload, headers
|
||||
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""
|
||||
Sends a Mattermost webhook notification using the given details.
|
||||
|
||||
The method constructs and dispatches a payload suitable for
|
||||
Mattermost's webhook system.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
url (str): The webhook URL to send the notification to.
|
||||
message (str): The main content or body of the notification message.
|
||||
bot_name (str): Override for the Webhook's name set on creation, see note!
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
|
||||
Note:
|
||||
- To set webhook username & pfp Mattermost needs to be configured to allow this!
|
||||
- Mattermost's `config.json` setting is `"EnablePostUsernameOverride": true`
|
||||
- Mattermost's `config.json` setting is `"EnablePostIconOverride": true`
|
||||
"""
|
||||
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||
payload, headers = self._construct_mattermost_payload(
|
||||
server_name, title, message, bot_name
|
||||
)
|
||||
return self._send_request(url, payload, headers)
|
98
app/classes/web/webhooks/slack_webhook.py
Normal file
@ -0,0 +1,98 @@
|
||||
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||
|
||||
|
||||
class SlackWebhook(WebhookProvider):
|
||||
def _construct_slack_payload(self, server_name, title, message, color, bot_name):
|
||||
"""
|
||||
Constructs the payload required for sending a Slack webhook notification.
|
||||
|
||||
The method formats the given information into a Markdown-styled message for MM,
|
||||
including an information card containing the Crafty version.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
message (str): The main content of the notification message.
|
||||
color (int): The color code for the side stripe in the Slack block.
|
||||
bot_name (str): Override for the Webhook's name set on creation, (not working).
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||
|
||||
Note:
|
||||
- Block Builder/designer
|
||||
- https://app.slack.com/block-kit-builder/
|
||||
"""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
payload = {
|
||||
"username": bot_name,
|
||||
"attachments": [
|
||||
{
|
||||
"color": color,
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "plain_text", "text": server_name},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"*{title}*\n{message}",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"image_url": self.WEBHOOK_PFP_URL,
|
||||
"alt_text": "Crafty Controller Logo",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": (
|
||||
f"*Crafty Controller "
|
||||
f"v{self.CRAFTY_VERSION}*"
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
{"type": "divider"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return payload, headers
|
||||
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""
|
||||
Sends a Slack webhook notification using the given details.
|
||||
|
||||
The method constructs and dispatches a payload suitable for
|
||||
Slack's webhook system.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
url (str): The webhook URL to send the notification to.
|
||||
message (str): The main content or body of the notification message.
|
||||
color (str, optional): The color code for the blocks's colour accent.
|
||||
Defaults to a pretty blue if not provided.
|
||||
bot_name (str): Override for the Webhook's name set on creation, (not working).
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
"""
|
||||
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
|
||||
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||
payload, headers = self._construct_slack_payload(
|
||||
server_name, title, message, color, bot_name
|
||||
)
|
||||
return self._send_request(url, payload, headers)
|
126
app/classes/web/webhooks/teams_adaptive_webhook.py
Normal file
@ -0,0 +1,126 @@
|
||||
from datetime import datetime
|
||||
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||
|
||||
|
||||
class TeamsWebhook(WebhookProvider):
|
||||
def _construct_teams_payload(self, server_name, title, message):
|
||||
"""
|
||||
Constructs the payload required for sending a Teams Adaptive card notification.
|
||||
|
||||
This method prepares a payload for the Teams webhook API using the provided
|
||||
message content, the Crafty Controller version, and the current UTC datetime.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
message (str): The main content of the notification message.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||
|
||||
Note:
|
||||
- Adaptive Card Designer
|
||||
- https://www.adaptivecards.io/designer/
|
||||
"""
|
||||
current_datetime = datetime.utcnow()
|
||||
formatted_datetime = current_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
headers = {"Content-type": "application/json"}
|
||||
payload = {
|
||||
"type": "message",
|
||||
"attachments": [
|
||||
{
|
||||
"contentType": "application/vnd.microsoft.card.adaptive",
|
||||
"content": {
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "Medium",
|
||||
"weight": "Bolder",
|
||||
"text": f"{title}",
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"style": "Person",
|
||||
"url": f"{self.WEBHOOK_PFP_URL}",
|
||||
"size": "Small",
|
||||
}
|
||||
],
|
||||
"width": "auto",
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"weight": "Bolder",
|
||||
"text": f"{server_name}",
|
||||
"wrap": True,
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"spacing": "None",
|
||||
"text": "{{DATE("
|
||||
+ f"{formatted_datetime}"
|
||||
+ ",SHORT)}}",
|
||||
"isSubtle": True,
|
||||
"wrap": True,
|
||||
},
|
||||
],
|
||||
"width": "stretch",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": f"{message}",
|
||||
"wrap": True,
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": f"Crafty Controller v{self.CRAFTY_VERSION}",
|
||||
"wrap": True,
|
||||
"separator": True,
|
||||
"isSubtle": True,
|
||||
},
|
||||
],
|
||||
"$schema": (
|
||||
"https://adaptivecards.io/schemas/adaptive-card.json"
|
||||
),
|
||||
"version": "1.6",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return payload, headers
|
||||
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""
|
||||
Sends a Teams Adaptive card notification using the given details.
|
||||
|
||||
The method constructs and dispatches a payload suitable for
|
||||
Discords's webhook system.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
url (str): The webhook URL to send the notification to.
|
||||
message (str): The main content or body of the notification message.
|
||||
Defaults to a pretty blue if not provided.
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
"""
|
||||
payload, headers = self._construct_teams_payload(server_name, title, message)
|
||||
return self._send_request(url, payload, headers)
|
84
app/classes/web/webhooks/webhook_factory.py
Normal file
@ -0,0 +1,84 @@
|
||||
from app.classes.web.webhooks.discord_webhook import DiscordWebhook
|
||||
from app.classes.web.webhooks.mattermost_webhook import MattermostWebhook
|
||||
from app.classes.web.webhooks.slack_webhook import SlackWebhook
|
||||
from app.classes.web.webhooks.teams_adaptive_webhook import TeamsWebhook
|
||||
|
||||
|
||||
class WebhookFactory:
|
||||
"""
|
||||
A factory class responsible for the creation and management of webhook providers.
|
||||
|
||||
This class provides methods to instantiate specific webhook providers based on
|
||||
their name and to retrieve a list of supported providers. It uses a registry pattern
|
||||
to manage the available providers.
|
||||
|
||||
Attributes:
|
||||
- _registry (dict): A dictionary mapping provider names to their classes.
|
||||
"""
|
||||
|
||||
_registry = {
|
||||
"Discord": DiscordWebhook,
|
||||
"Mattermost": MattermostWebhook,
|
||||
"Slack": SlackWebhook,
|
||||
"Teams": TeamsWebhook,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_provider(cls, provider_name, *args, **kwargs):
|
||||
"""
|
||||
Creates and returns an instance of the specified webhook provider.
|
||||
|
||||
This method looks up the provider in the registry, then instantiates it w/ the
|
||||
provided arguments. If the provider is not recognized, a ValueError is raised.
|
||||
|
||||
Arguments:
|
||||
- provider_name (str): The name of the desired webhook provider.
|
||||
|
||||
Additional arguments supported that we may use for if a provider
|
||||
requires initialization:
|
||||
- *args: Positional arguments to pass to the provider's constructor.
|
||||
- **kwargs: Keyword arguments to pass to the provider's constructor.
|
||||
|
||||
Returns:
|
||||
WebhookProvider: An instance of the desired webhook provider.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified provider name is not recognized.
|
||||
"""
|
||||
if provider_name not in cls._registry:
|
||||
raise ValueError(f"Provider {provider_name} is not supported.")
|
||||
return cls._registry[provider_name](*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_supported_providers(cls):
|
||||
"""
|
||||
Retrieves the names of all supported webhook providers.
|
||||
|
||||
This method returns a list containing the names of all providers
|
||||
currently registered in the factory's registry.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of supported provider names.
|
||||
"""
|
||||
return list(cls._registry.keys())
|
||||
|
||||
@staticmethod
|
||||
def get_monitored_events():
|
||||
"""
|
||||
Retrieves the list of supported events for monitoring.
|
||||
|
||||
This method provides a list of common server events that the webhook system can
|
||||
monitor and notify about.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of supported monitored actions.
|
||||
"""
|
||||
return [
|
||||
"start_server",
|
||||
"stop_server",
|
||||
"crash_detected",
|
||||
"backup_server",
|
||||
"jar_update",
|
||||
"send_command",
|
||||
"kill",
|
||||
]
|
@ -4,15 +4,17 @@ import asyncio
|
||||
from urllib.parse import parse_qsl
|
||||
import tornado.websocket
|
||||
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
page = None
|
||||
page_query_params = None
|
||||
controller = None
|
||||
controller: Controller = None
|
||||
tasks_manager = None
|
||||
translator = None
|
||||
io_loop = None
|
||||
@ -40,23 +42,16 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
)
|
||||
return remote_ip
|
||||
|
||||
def get_user_id(self):
|
||||
_, _, user = self.controller.authentication.check(self.get_cookie("token"))
|
||||
return user["user_id"]
|
||||
|
||||
def check_auth(self):
|
||||
return self.controller.authentication.check_bool(self.get_cookie("token"))
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def open(self):
|
||||
logger.debug("Checking WebSocket authentication")
|
||||
if self.check_auth():
|
||||
self.handle()
|
||||
else:
|
||||
self.helper.websocket_helper.send_message(
|
||||
WebSocketManager().broadcast_to_admins(
|
||||
self, "notification", "Not authenticated for WebSocket connection"
|
||||
)
|
||||
self.close()
|
||||
self.close(1011, "Forbidden WS Access")
|
||||
self.controller.management.add_to_audit_log_raw(
|
||||
"unknown",
|
||||
0,
|
||||
@ -64,7 +59,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
"Someone tried to connect via WebSocket without proper authentication",
|
||||
self.get_remote_ip(),
|
||||
)
|
||||
self.helper.websocket_helper.broadcast(
|
||||
WebSocketManager().broadcast(
|
||||
"notification",
|
||||
"Someone tried to connect via WebSocket without proper authentication",
|
||||
)
|
||||
@ -79,24 +74,34 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
Helpers.remove_prefix(self.get_query_argument("page_query_params"), "?")
|
||||
)
|
||||
)
|
||||
self.helper.websocket_helper.add_client(self)
|
||||
WebSocketManager().add_client(self)
|
||||
logger.debug("Opened WebSocket connection")
|
||||
|
||||
# pylint: disable=arguments-renamed
|
||||
@staticmethod
|
||||
def on_message(raw_message):
|
||||
def on_message(self, raw_message):
|
||||
logger.debug(f"Got message from WebSocket connection {raw_message}")
|
||||
message = json.loads(raw_message)
|
||||
logger.debug(f"Event Type: {message['event']}, Data: {message['data']}")
|
||||
|
||||
def on_close(self):
|
||||
self.helper.websocket_helper.remove_client(self)
|
||||
WebSocketManager().remove_client(self)
|
||||
logger.debug("Closed WebSocket connection")
|
||||
|
||||
async def write_message_int(self, message):
|
||||
self.write_message(message)
|
||||
|
||||
def write_message_helper(self, message):
|
||||
def write_message_async(self, message):
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.write_message_int(message), self.io_loop.asyncio_loop
|
||||
)
|
||||
|
||||
def send_message(self, event_type: str, data):
|
||||
message = str(json.dumps({"event": event_type, "data": data}))
|
||||
self.write_message_async(message)
|
||||
|
||||
def get_user_id(self):
|
||||
_, _, user = self.controller.authentication.check(self.get_cookie("token"))
|
||||
return user["user_id"]
|
||||
|
||||
def check_auth(self):
|
||||
return self.controller.authentication.check_bool(self.get_cookie("token"))
|
||||
|
@ -10,16 +10,17 @@
|
||||
},
|
||||
"schedule": {
|
||||
"format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s"
|
||||
},
|
||||
"auth": {
|
||||
"format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "commander",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
|
||||
"main_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "commander",
|
||||
@ -50,24 +51,45 @@
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 20,
|
||||
"encoding": "utf8"
|
||||
},
|
||||
"auth_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "auth",
|
||||
"filename": "logs/auth.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 20,
|
||||
"encoding": "utf8"
|
||||
}
|
||||
},
|
||||
|
||||
"loggers": {
|
||||
"": {
|
||||
"level": "INFO",
|
||||
"handlers": ["main_file_handler", "session_file_handler"],
|
||||
"handlers": [
|
||||
"main_file_handler",
|
||||
"session_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
},
|
||||
"tornado.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["tornado_access_file_handler"],
|
||||
"handlers": [
|
||||
"tornado_access_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
},
|
||||
"apscheduler": {
|
||||
"level": "INFO",
|
||||
"handlers": ["schedule_file_handler"],
|
||||
"handlers": [
|
||||
"schedule_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
},
|
||||
"auth": {
|
||||
"level": "INFO",
|
||||
"handlers": [
|
||||
"auth_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"major": 4,
|
||||
"minor": 2,
|
||||
"sub": 0
|
||||
"sub": 2
|
||||
}
|
||||
|
447
app/frontend/static/assets/css/crafty-toggle-btn.css
Normal file
@ -0,0 +1,447 @@
|
||||
/**************************************************************/
|
||||
/* CSS for Toggle Buttons */
|
||||
/**************************************************************/
|
||||
.btn-toggle {
|
||||
margin: 0 4rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
border: none;
|
||||
height: 1.5rem;
|
||||
width: 3rem;
|
||||
border-radius: 1.5rem;
|
||||
color: #6b7381;
|
||||
background: #bdc1c8;
|
||||
}
|
||||
|
||||
.btn-toggle:focus,
|
||||
.btn-toggle.focus,
|
||||
.btn-toggle:focus.active,
|
||||
.btn-toggle.focus.active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-toggle:before,
|
||||
.btn-toggle:after {
|
||||
line-height: 1.5rem;
|
||||
width: 4rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle:before {
|
||||
content: 'Off';
|
||||
left: -4rem;
|
||||
}
|
||||
|
||||
.btn-toggle:after {
|
||||
content: 'On';
|
||||
right: -4rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle>.handle {
|
||||
position: absolute;
|
||||
top: 0.1875rem;
|
||||
left: 0.1875rem;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
border-radius: 1.125rem;
|
||||
background: #fff;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.active {
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.active>.handle {
|
||||
left: 1.6875rem;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.active:before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:before,
|
||||
.btn-toggle.btn-sm:after {
|
||||
line-height: -0.5rem;
|
||||
color: #fff;
|
||||
letter-spacing: 0.75px;
|
||||
left: 0.4125rem;
|
||||
width: 2.325rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:before {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:after {
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.active:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs:before,
|
||||
.btn-toggle.btn-xs:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-toggle:before,
|
||||
.btn-toggle:after {
|
||||
color: #6b7381;
|
||||
}
|
||||
|
||||
.btn-toggle.active {
|
||||
background-color: #29b5a8;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg {
|
||||
margin: 0 5rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
border: none;
|
||||
height: 2.5rem;
|
||||
width: 5rem;
|
||||
border-radius: 2.5rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg:focus,
|
||||
.btn-toggle.btn-lg.focus,
|
||||
.btn-toggle.btn-lg:focus.active,
|
||||
.btn-toggle.btn-lg.focus.active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg:before,
|
||||
.btn-toggle.btn-lg:after {
|
||||
line-height: 2.5rem;
|
||||
width: 5rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg:before {
|
||||
content: 'Off';
|
||||
left: -5rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg:after {
|
||||
content: 'On';
|
||||
right: -5rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg>.handle {
|
||||
position: absolute;
|
||||
top: 0.3125rem;
|
||||
left: 0.3125rem;
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
border-radius: 1.875rem;
|
||||
background: #fff;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.active {
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.active>.handle {
|
||||
left: 2.8125rem;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.active:before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.btn-sm:before,
|
||||
.btn-toggle.btn-lg.btn-sm:after {
|
||||
line-height: 0.5rem;
|
||||
color: #fff;
|
||||
letter-spacing: 0.75px;
|
||||
left: 0.6875rem;
|
||||
width: 3.875rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.btn-sm:before {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.btn-sm:after {
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.btn-sm.active:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.btn-sm.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-lg.btn-xs:before,
|
||||
.btn-toggle.btn-lg.btn-xs:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm {
|
||||
margin: 0 0.5rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
border: none;
|
||||
height: 1.5rem;
|
||||
width: 3rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:focus,
|
||||
.btn-toggle.btn-sm.focus,
|
||||
.btn-toggle.btn-sm:focus.active,
|
||||
.btn-toggle.btn-sm.focus.active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:before,
|
||||
.btn-toggle.btn-sm:after {
|
||||
line-height: 1.5rem;
|
||||
width: 0.5rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.55rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:before {
|
||||
content: 'Off';
|
||||
left: -0.5rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm:after {
|
||||
content: 'On';
|
||||
right: -0.5rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm>.handle {
|
||||
position: absolute;
|
||||
top: 0.1875rem;
|
||||
left: 0.1875rem;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
border-radius: 1.125rem;
|
||||
background: #fff;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.active {
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.active>.handle {
|
||||
left: 1.6875rem;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.active:before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.btn-sm:before,
|
||||
.btn-toggle.btn-sm.btn-sm:after {
|
||||
line-height: -0.5rem;
|
||||
color: #fff;
|
||||
letter-spacing: 0.75px;
|
||||
left: 0.4125rem;
|
||||
width: 2.325rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.btn-sm:before {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.btn-sm:after {
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.btn-sm.active:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.btn-sm.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-sm.btn-xs:before,
|
||||
.btn-toggle.btn-sm.btn-xs:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs {
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
border: none;
|
||||
height: 1rem;
|
||||
width: 2rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs:focus,
|
||||
.btn-toggle.btn-xs.focus,
|
||||
.btn-toggle.btn-xs:focus.active,
|
||||
.btn-toggle.btn-xs.focus.active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs:before,
|
||||
.btn-toggle.btn-xs:after {
|
||||
line-height: 1rem;
|
||||
width: 0;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs:before {
|
||||
content: 'Off';
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs:after {
|
||||
content: 'On';
|
||||
right: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs>.handle {
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
left: 0.125rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
background: #fff;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.active {
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.active>.handle {
|
||||
left: 1.125rem;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.active:before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.btn-sm:before,
|
||||
.btn-toggle.btn-xs.btn-sm:after {
|
||||
line-height: -1rem;
|
||||
color: #fff;
|
||||
letter-spacing: 0.75px;
|
||||
left: 0.275rem;
|
||||
width: 1.55rem;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.btn-sm:before {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.btn-sm:after {
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.btn-sm.active:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.btn-sm.active:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-xs.btn-xs:before,
|
||||
.btn-toggle.btn-xs.btn-xs:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-info {
|
||||
color: var(--white);
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
.btn-toggle.btn-info:before,
|
||||
.btn-toggle.btn-info:after {
|
||||
color: #6b7381;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-info.active {
|
||||
background-color: var(--info);
|
||||
}
|
||||
|
||||
.btn-toggle.btn-secondary {
|
||||
color: #6b7381;
|
||||
background: #bdc1c8;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-secondary:before,
|
||||
.btn-toggle.btn-secondary:after {
|
||||
color: #6b7381;
|
||||
}
|
||||
|
||||
.btn-toggle.btn-secondary.active {
|
||||
background-color: #ff8300;
|
||||
}
|
||||
|
||||
/**************************************************************/
|
@ -182,11 +182,55 @@ div>.input-group>.form-control {
|
||||
}
|
||||
|
||||
.no-scroll {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
.no-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-label::before,
|
||||
.custom-control-label::after {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-control-input:checked~.custom-control-label::before {
|
||||
color: black !important;
|
||||
background-color: blueviolet !important;
|
||||
border-color: var(--outline) !important;
|
||||
}
|
||||
|
||||
.custom-control-label::before {
|
||||
background-color: white !important;
|
||||
top: calc(-0.2rem);
|
||||
}
|
||||
|
||||
.custom-switch .custom-control-label::after {
|
||||
top: calc(-0.125rem + 1px);
|
||||
}
|
||||
|
||||
/**************************************************************/
|
||||
|
||||
/**************************************************************/
|
||||
/* CSS for Tables Displays */
|
||||
/**************************************************************/
|
||||
td>ul {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td p {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td.action {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
td.action .btn {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
/**************************************************************/
|
@ -22979,27 +22979,42 @@ ul li {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tab-simple-styled {
|
||||
.nav-tabs.tab-simple-styled {
|
||||
border-bottom: none;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tab-simple-styled .nav-item {
|
||||
.nav-tabs.tab-simple-styled .nav-item {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.tab-simple-styled .nav-item .nav-link {
|
||||
.nav-tabs.tab-simple-styled .nav-item .nav-link {
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--base-text);
|
||||
}
|
||||
|
||||
.tab-simple-styled .nav-item .nav-link.active {
|
||||
.nav-tabs.tab-simple-styled .nav-item .nav-link.active {
|
||||
background: var(--dropdown-bg);
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.nav-pills.tab-simple-styled {
|
||||
border-bottom: none;
|
||||
/*margin-top: 1.5rem;*/
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/*.nav-pills.tab-simple-styled .nav-item {
|
||||
margin-right: 1.5rem;
|
||||
}*/
|
||||
|
||||
.nav-pills.tab-simple-styled .nav-item .nav-link.active {
|
||||
background: var(--info);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab-tile-style {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
|
@ -21494,27 +21494,42 @@ ul li {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tab-simple-styled {
|
||||
.nav-tabs.tab-simple-styled {
|
||||
border-bottom: none;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tab-simple-styled .nav-item {
|
||||
.nav-tabs.tab-simple-styled .nav-item {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.tab-simple-styled .nav-item .nav-link {
|
||||
.nav-tabs.tab-simple-styled .nav-item .nav-link {
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.tab-simple-styled .nav-item .nav-link.active {
|
||||
.nav-tabs.tab-simple-styled .nav-item .nav-link.active {
|
||||
background: #fff;
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.nav-pills.tab-simple-styled {
|
||||
border-bottom: none;
|
||||
/*margin-top: 1.5rem;*/
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/*.nav-pills.tab-simple-styled .nav-item {
|
||||
margin-right: 1.5rem;
|
||||
}*/
|
||||
|
||||
.nav-pills.tab-simple-styled .nav-item .nav-link.active {
|
||||
background: var(--info);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab-tile-style {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
10
app/frontend/static/assets/images/crafty-logo-square.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425 425">
|
||||
<!-- Background -->
|
||||
<rect x="0" y="0" rx="105" ry="105" width="425" height="425" style="fill: #22283A"/>
|
||||
<!-- green C -->
|
||||
<path fill="hsl(160, 59%, 45%)" d="M 162.5 182.5 l 38 -15 q 0 -40 -40 -40 l -95 0 q -40 0 -40 40 l 0 90 q 0 40 40 40 l 95 0 q 40 0 40 -40 l -38 -20 l 0 28 l -100 0 l 0 -108 l 100 0 z"/>
|
||||
<!-- blue C -->
|
||||
<path fill="hsl(192, 99%, 45%)" d="M 262.5 182.5 l -38 -15 q 0 -40 40 -40 l 95 0 q 40 0 40 40 l 0 90 q 0 40 -40 40 l -95 0 q -40 0 -40 -40 l 38 -20 l 0 28 l 100 0 l 0 -108 l -100 0 z"/>
|
||||
<!-- line thing in middle of Cs -->
|
||||
<path fill="hsl(266, 71%, 57%)" d="M 142.5 191.5 q -10 0 -10 10 l 0 19 q 0 10 10 10 l 140 0 q 10 0 10 -10 l 0 -19 q 0 -10 -10 -10 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 750 B |
@ -6,36 +6,9 @@ importScripts(
|
||||
|
||||
const CACHE = "crafty-controller";
|
||||
|
||||
// TODO: replace the following with the correct offline fallback page i.e.: const offlineFallbackPage = "offline.html";
|
||||
const offlineFallbackPage = "/offline";
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
//This service worker is basically just here to make browsers
|
||||
//accept the PWA. It's not doing much anymore
|
||||
|
||||
if (workbox.navigationPreload.isSupported()) {
|
||||
workbox.navigationPreload.enable();
|
||||
}
|
||||
|
||||
// self.addEventListener('fetch', (event) => {
|
||||
// if (event.request.mode === 'navigate') {
|
||||
// event.respondWith((async () => {
|
||||
// try {
|
||||
// const preloadResp = await event.preloadResponse;
|
||||
|
||||
// if (preloadResp) {
|
||||
// return preloadResp;
|
||||
// }
|
||||
// const networkResp = await fetch(event.request);
|
||||
// return networkResp;
|
||||
// } catch (error) {
|
||||
|
||||
// const cache = await caches.open(CACHE);
|
||||
// const cachedResp = await cache.match(offlineFallbackPage);
|
||||
// return cachedResp;
|
||||
// }
|
||||
// })());
|
||||
// }
|
||||
// });
|
||||
|
28
app/frontend/static/assets/vendors/css/bootstrap-toggle.min.css
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
/*! ========================================================================
|
||||
* Bootstrap Toggle: bootstrap-toggle.css v2.2.0
|
||||
* http://www.bootstraptoggle.com
|
||||
* ========================================================================
|
||||
* Copyright 2014 Min Hur, The New York Times Company
|
||||
* Licensed under MIT
|
||||
* ======================================================================== */
|
||||
.checkbox label .toggle,.checkbox-inline .toggle{margin-left:-20px;margin-right:5px}
|
||||
.toggle{position:relative;overflow:hidden}
|
||||
.toggle input[type=checkbox]{display:none}
|
||||
.toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none}
|
||||
.toggle.off .toggle-group{left:-100%}
|
||||
.toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0}
|
||||
.toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0}
|
||||
.toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px}
|
||||
.toggle.btn{min-width:59px;min-height:34px}
|
||||
.toggle-on.btn{padding-right:24px}
|
||||
.toggle-off.btn{padding-left:24px}
|
||||
.toggle.btn-lg{min-width:79px;min-height:45px}
|
||||
.toggle-on.btn-lg{padding-right:31px}
|
||||
.toggle-off.btn-lg{padding-left:31px}
|
||||
.toggle-handle.btn-lg{width:40px}
|
||||
.toggle.btn-sm{min-width:50px;min-height:30px}
|
||||
.toggle-on.btn-sm{padding-right:20px}
|
||||
.toggle-off.btn-sm{padding-left:20px}
|
||||
.toggle.btn-xs{min-width:35px;min-height:22px}
|
||||
.toggle-on.btn-xs{padding-right:12px}
|
||||
.toggle-off.btn-xs{padding-left:12px}
|
1
app/frontend/static/assets/vendors/js/bootbox.min.js
vendored
Normal file
9
app/frontend/static/assets/vendors/js/bootstrap-select.min.js
vendored
Normal file
9
app/frontend/static/assets/vendors/js/bootstrap-toggle.min.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/*! ========================================================================
|
||||
* Bootstrap Toggle: bootstrap-toggle.js v2.2.0
|
||||
* http://www.bootstraptoggle.com
|
||||
* ========================================================================
|
||||
* Copyright 2014 Min Hur, The New York Times Company
|
||||
* Licensed under MIT
|
||||
* ======================================================================== */
|
||||
+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-lg":"small"===this.options.size?"btn-sm":"mini"===this.options.size?"btn-xs":"",c=a('<label class="btn">').html(this.options.on).addClass(this._onstyle+" "+b),d=a('<label class="btn">').html(this.options.off).addClass(this._offstyle+" "+b+" active"),e=a('<span class="toggle-handle btn btn-default">').addClass(b),f=a('<div class="toggle-group">').append(c,d,e),g=a('<div class="toggle btn" data-toggle="toggle">').addClass(this.$element.prop("checked")?this._onstyle:this._offstyle+" off").addClass(b).addClass(this.options.style);this.$element.wrap(g),a.extend(this,{$toggle:this.$element.parent(),$toggleOn:c,$toggleOff:d,$toggleGroup:f}),this.$toggle.append(f);var h=this.options.width||Math.max(c.outerWidth(),d.outerWidth())+e.outerWidth()/2,i=this.options.height||Math.max(c.outerHeight(),d.outerHeight());c.addClass("toggle-on"),d.addClass("toggle-off"),this.$toggle.css({width:h,height:i}),this.options.height&&(c.css("line-height",c.height()+"px"),d.css("line-height",d.height()+"px")),this.update(!0),this.trigger(!0)},c.prototype.toggle=function(){this.$element.prop("checked")?this.off():this.on()},c.prototype.on=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._offstyle+" off").addClass(this._onstyle),this.$element.prop("checked",!0),void(a||this.trigger()))},c.prototype.off=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._onstyle).addClass(this._offstyle+" off"),this.$element.prop("checked",!1),void(a||this.trigger()))},c.prototype.enable=function(){this.$toggle.removeAttr("disabled"),this.$element.prop("disabled",!1)},c.prototype.disable=function(){this.$toggle.attr("disabled","disabled"),this.$element.prop("disabled",!0)},c.prototype.update=function(a){this.$element.prop("disabled")?this.disable():this.enable(),this.$element.prop("checked")?this.on(a):this.off(a)},c.prototype.trigger=function(b){this.$element.off("change.bs.toggle"),b||this.$element.change(),this.$element.on("change.bs.toggle",a.proxy(function(){this.update()},this))},c.prototype.destroy=function(){this.$element.off("change.bs.toggle"),this.$toggleGroup.remove(),this.$element.removeData("bs.toggle"),this.$element.unwrap()};var d=a.fn.bootstrapToggle;a.fn.bootstrapToggle=b,a.fn.bootstrapToggle.Constructor=c,a.fn.toggle.noConflict=function(){return a.fn.bootstrapToggle=d,this},a(function(){a("input[type=checkbox][data-toggle^=toggle]").bootstrapToggle()}),a(document).on("click.bs.toggle","div[data-toggle^=toggle]",function(b){var c=a(this).find("input[type=checkbox]");c.bootstrapToggle("toggle"),b.preventDefault()})}(jQuery);
|
||||
//# sourceMappingURL=bootstrap-toggle.min.js.map
|
5
app/frontend/static/assets/vendors/js/cdn.min.js
vendored
Normal file
13
app/frontend/static/assets/vendors/js/chart.min.js
vendored
Normal file
7
app/frontend/static/assets/vendors/js/chartjs-plugin-zoom.min.js
vendored
Normal file
491
app/frontend/static/assets/vendors/js/datatables.min.js
vendored
Normal file
@ -0,0 +1,491 @@
|
||||
/*
|
||||
* This combined file was created by the DataTables downloader builder:
|
||||
* https://datatables.net/download
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.10.22, FixedHeader 3.1.7, Responsive 2.2.6, Scroller 2.0.3, SearchPanes 1.2.2
|
||||
*/
|
||||
|
||||
/*!
|
||||
Copyright 2008-2020 SpryMedia Ltd.
|
||||
|
||||
This source file is free software, available under the following license:
|
||||
MIT license - http://datatables.net/license
|
||||
|
||||
This source file is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||
|
||||
For details please refer to: http://www.datatables.net
|
||||
DataTables 1.10.22
|
||||
©2008-2020 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(k,y,z){k instanceof String&&(k=String(k));for(var q=k.length,G=0;G<q;G++){var O=k[G];if(y.call(z,O,G,k))return{i:G,v:O}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(k,y,z){if(k==Array.prototype||k==Object.prototype)return k;k[y]=z.value;return k};$jscomp.getGlobal=function(k){k=["object"==typeof globalThis&&globalThis,k,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var y=0;y<k.length;++y){var z=k[y];if(z&&z.Math==Math)return z}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(k,y){var z=$jscomp.propertyToPolyfillSymbol[y];if(null==z)return k[y];z=k[z];return void 0!==z?z:k[y]};
|
||||
$jscomp.polyfill=function(k,y,z,q){y&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(k,y,z,q):$jscomp.polyfillUnisolated(k,y,z,q))};$jscomp.polyfillUnisolated=function(k,y,z,q){z=$jscomp.global;k=k.split(".");for(q=0;q<k.length-1;q++){var G=k[q];if(!(G in z))return;z=z[G]}k=k[k.length-1];q=z[k];y=y(q);y!=q&&null!=y&&$jscomp.defineProperty(z,k,{configurable:!0,writable:!0,value:y})};
|
||||
$jscomp.polyfillIsolated=function(k,y,z,q){var G=k.split(".");k=1===G.length;q=G[0];q=!k&&q in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var O=0;O<G.length-1;O++){var ma=G[O];if(!(ma in q))return;q=q[ma]}G=G[G.length-1];z=$jscomp.IS_SYMBOL_NATIVE&&"es6"===z?q[G]:null;y=y(z);null!=y&&(k?$jscomp.defineProperty($jscomp.polyfills,G,{configurable:!0,writable:!0,value:y}):y!==z&&($jscomp.propertyToPolyfillSymbol[G]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(G):$jscomp.POLYFILL_PREFIX+G,
|
||||
G=$jscomp.propertyToPolyfillSymbol[G],$jscomp.defineProperty(q,G,{configurable:!0,writable:!0,value:y})))};$jscomp.polyfill("Array.prototype.find",function(k){return k?k:function(y,z){return $jscomp.findInternal(this,y,z).v}},"es6","es3");
|
||||
(function(k){"function"===typeof define&&define.amd?define(["jquery"],function(y){return k(y,window,document)}):"object"===typeof exports?module.exports=function(y,z){y||(y=window);z||(z="undefined"!==typeof window?require("jquery"):require("jquery")(y));return k(z,y,y.document)}:k(jQuery,window,document)})(function(k,y,z,q){function G(a){var b,c,d={};k.each(a,function(f,e){(b=f.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" ")&&(c=f.replace(b[0],b[2].toLowerCase()),
|
||||
d[c]=f,"o"===b[1]&&G(a[f]))});a._hungarianMap=d}function O(a,b,c){a._hungarianMap||G(a);var d;k.each(b,function(f,e){d=a._hungarianMap[f];d===q||!c&&b[d]!==q||("o"===d.charAt(0)?(b[d]||(b[d]={}),k.extend(!0,b[d],b[f]),O(a[d],b[d],c)):b[d]=b[f])})}function ma(a){var b=u.defaults.oLanguage,c=b.sDecimal;c&&Va(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&d&&"No data available in table"===b.sEmptyTable&&V(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&d&&"Loading..."===b.sLoadingRecords&&V(a,a,
|
||||
"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Va(a)}}function yb(a){R(a,"ordering","bSort");R(a,"orderMulti","bSortMulti");R(a,"orderClasses","bSortClasses");R(a,"orderCellsTop","bSortCellsTop");R(a,"order","aaSorting");R(a,"orderFixed","aaSortingFixed");R(a,"paging","bPaginate");R(a,"pagingType","sPaginationType");R(a,"pageLength","iDisplayLength");R(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":
|
||||
"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&O(u.models.oSearch,a[b])}function zb(a){R(a,"orderable","bSortable");R(a,"orderData","aDataSort");R(a,"orderSequence","asSorting");R(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"!==typeof b||Array.isArray(b)||(a.aDataSort=[b])}function Ab(a){if(!u.__browser){var b={};u.__browser=b;var c=k("<div/>").css({position:"fixed",top:0,left:-1*k(y).scrollLeft(),height:1,
|
||||
width:1,overflow:"hidden"}).append(k("<div/>").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(k("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),f=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===f[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(f.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}k.extend(a.oBrowser,u.__browser);a.oScroll.iBarWidth=u.__browser.barWidth}
|
||||
function Bb(a,b,c,d,f,e){var g=!1;if(c!==q){var h=c;g=!0}for(;d!==f;)a.hasOwnProperty(d)&&(h=g?b(h,a[d],d,a):a[d],g=!0,d+=e);return h}function Wa(a,b){var c=u.defaults.column,d=a.aoColumns.length;c=k.extend({},u.models.oColumn,c,{nTh:b?b:z.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=k.extend({},u.models.oSearch,c[d]);Da(a,d,k(b).data())}function Da(a,b,c){b=a.aoColumns[b];
|
||||
var d=a.oClasses,f=k(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=f.attr("width")||null;var e=(f.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);e&&(b.sWidthOrig=e[1])}c!==q&&null!==c&&(zb(c),O(u.defaults.column,c,!0),c.mDataProp===q||c.mData||(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&f.addClass(c.sClass),k.extend(b,c),V(b,c,"sWidth","sWidthOrig"),c.iDataSort!==q&&(b.aDataSort=[c.iDataSort]),V(b,c,"aDataSort"));var g=b.mData,h=ia(g),
|
||||
l=b.mRender?ia(b.mRender):null;c=function(n){return"string"===typeof n&&-1!==n.indexOf("@")};b._bAttrSrc=k.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(n,m,p){var t=h(n,m,q,p);return l&&m?l(t,m,n,p):t};b.fnSetData=function(n,m,p){return da(g)(n,m,p)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,f.addClass(d.sSortableNone));a=-1!==k.inArray("asc",b.asSorting);c=-1!==k.inArray("desc",b.asSorting);b.bSortable&&(a||c)?a&&!c?
|
||||
(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI):(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI="")}function ra(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Xa(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;""===b.sY&&""===b.sX||Ea(a);I(a,null,"column-sizing",[a])}function sa(a,b){a=Fa(a,"bVisible");
|
||||
return"number"===typeof a[b]?a[b]:null}function ta(a,b){a=Fa(a,"bVisible");b=k.inArray(b,a);return-1!==b?b:null}function na(a){var b=0;k.each(a.aoColumns,function(c,d){d.bVisible&&"none"!==k(d.nTh).css("display")&&b++});return b}function Fa(a,b){var c=[];k.map(a.aoColumns,function(d,f){d[b]&&c.push(f)});return c}function Ya(a){var b=a.aoColumns,c=a.aoData,d=u.ext.type.detect,f,e,g;var h=0;for(f=b.length;h<f;h++){var l=b[h];var n=[];if(!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){var m=
|
||||
0;for(e=d.length;m<e;m++){var p=0;for(g=c.length;p<g;p++){n[p]===q&&(n[p]=S(a,p,h,"type"));var t=d[m](n[p],a);if(!t&&m!==d.length-1)break;if("html"===t)break}if(t){l.sType=t;break}}l.sType||(l.sType="string")}}}function Cb(a,b,c,d){var f,e,g,h=a.aoColumns;if(b)for(f=b.length-1;0<=f;f--){var l=b[f];var n=l.targets!==q?l.targets:l.aTargets;Array.isArray(n)||(n=[n]);var m=0;for(e=n.length;m<e;m++)if("number"===typeof n[m]&&0<=n[m]){for(;h.length<=n[m];)Wa(a);d(n[m],l)}else if("number"===typeof n[m]&&
|
||||
0>n[m])d(h.length+n[m],l);else if("string"===typeof n[m]){var p=0;for(g=h.length;p<g;p++)("_all"==n[m]||k(h[p].nTh).hasClass(n[m]))&&d(p,l)}}if(c)for(f=0,a=c.length;f<a;f++)d(f,c[f])}function ea(a,b,c,d){var f=a.aoData.length,e=k.extend(!0,{},u.models.oRow,{src:c?"dom":"data",idx:f});e._aData=b;a.aoData.push(e);for(var g=a.aoColumns,h=0,l=g.length;h<l;h++)g[h].sType=null;a.aiDisplayMaster.push(f);b=a.rowIdFn(b);b!==q&&(a.aIds[b]=e);!c&&a.oFeatures.bDeferRender||Za(a,f,c,d);return f}function Ga(a,
|
||||
b){var c;b instanceof k||(b=k(b));return b.map(function(d,f){c=$a(a,f);return ea(a,c.data,f,c.cells)})}function S(a,b,c,d){var f=a.iDraw,e=a.aoColumns[c],g=a.aoData[b]._aData,h=e.sDefaultContent,l=e.fnGetData(g,d,{settings:a,row:b,col:c});if(l===q)return a.iDrawError!=f&&null===h&&(aa(a,0,"Requested unknown parameter "+("function"==typeof e.mData?"{function}":"'"+e.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=f),h;if((l===g||null===l)&&null!==h&&d!==q)l=h;else if("function"===typeof l)return l.call(g);
|
||||
return null===l&&"display"==d?"":l}function Db(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}function ab(a){return k.map(a.match(/(\\.|[^\.])+/g)||[""],function(b){return b.replace(/\\\./g,".")})}function ia(a){if(k.isPlainObject(a)){var b={};k.each(a,function(d,f){f&&(b[d]=ia(f))});return function(d,f,e,g){var h=b[f]||b._;return h!==q?h(d,f,e,g):d}}if(null===a)return function(d){return d};if("function"===typeof a)return function(d,f,e,g){return a(d,f,e,g)};if("string"!==
|
||||
typeof a||-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("("))return function(d,f){return d[a]};var c=function(d,f,e){if(""!==e){var g=ab(e);for(var h=0,l=g.length;h<l;h++){e=g[h].match(ua);var n=g[h].match(oa);if(e){g[h]=g[h].replace(ua,"");""!==g[h]&&(d=d[g[h]]);n=[];g.splice(0,h+1);g=g.join(".");if(Array.isArray(d))for(h=0,l=d.length;h<l;h++)n.push(c(d[h],f,g));d=e[0].substring(1,e[0].length-1);d=""===d?n:n.join(d);break}else if(n){g[h]=g[h].replace(oa,"");d=d[g[h]]();continue}if(null===
|
||||
d||d[g[h]]===q)return q;d=d[g[h]]}}return d};return function(d,f){return c(d,f,a)}}function da(a){if(k.isPlainObject(a))return da(a._);if(null===a)return function(){};if("function"===typeof a)return function(c,d,f){a(c,"set",d,f)};if("string"!==typeof a||-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("("))return function(c,d){c[a]=d};var b=function(c,d,f){f=ab(f);var e=f[f.length-1];for(var g,h,l=0,n=f.length-1;l<n;l++){if("__proto__"===f[l])throw Error("Cannot set prototype values");g=
|
||||
f[l].match(ua);h=f[l].match(oa);if(g){f[l]=f[l].replace(ua,"");c[f[l]]=[];e=f.slice();e.splice(0,l+1);g=e.join(".");if(Array.isArray(d))for(h=0,n=d.length;h<n;h++)e={},b(e,d[h],g),c[f[l]].push(e);else c[f[l]]=d;return}h&&(f[l]=f[l].replace(oa,""),c=c[f[l]](d));if(null===c[f[l]]||c[f[l]]===q)c[f[l]]={};c=c[f[l]]}if(e.match(oa))c[e.replace(oa,"")](d);else c[e.replace(ua,"")]=d};return function(c,d){return b(c,d,a)}}function bb(a){return T(a.aoData,"_aData")}function Ha(a){a.aoData.length=0;a.aiDisplayMaster.length=
|
||||
0;a.aiDisplay.length=0;a.aIds={}}function Ia(a,b,c){for(var d=-1,f=0,e=a.length;f<e;f++)a[f]==b?d=f:a[f]>b&&a[f]--; -1!=d&&c===q&&a.splice(d,1)}function va(a,b,c,d){var f=a.aoData[b],e,g=function(l,n){for(;l.childNodes.length;)l.removeChild(l.firstChild);l.innerHTML=S(a,b,n,"display")};if("dom"!==c&&(c&&"auto"!==c||"dom"!==f.src)){var h=f.anCells;if(h)if(d!==q)g(h[d],d);else for(c=0,e=h.length;c<e;c++)g(h[c],c)}else f._aData=$a(a,f,d,d===q?q:f._aData).data;f._aSortData=null;f._aFilterData=null;g=
|
||||
a.aoColumns;if(d!==q)g[d].sType=null;else{c=0;for(e=g.length;c<e;c++)g[c].sType=null;cb(a,f)}}function $a(a,b,c,d){var f=[],e=b.firstChild,g,h=0,l,n=a.aoColumns,m=a._rowReadObject;d=d!==q?d:m?{}:[];var p=function(x,r){if("string"===typeof x){var A=x.indexOf("@");-1!==A&&(A=x.substring(A+1),da(x)(d,r.getAttribute(A)))}},t=function(x){if(c===q||c===h)g=n[h],l=x.innerHTML.trim(),g&&g._bAttrSrc?(da(g.mData._)(d,l),p(g.mData.sort,x),p(g.mData.type,x),p(g.mData.filter,x)):m?(g._setter||(g._setter=da(g.mData)),
|
||||
g._setter(d,l)):d[h]=l;h++};if(e)for(;e;){var v=e.nodeName.toUpperCase();if("TD"==v||"TH"==v)t(e),f.push(e);e=e.nextSibling}else for(f=b.anCells,e=0,v=f.length;e<v;e++)t(f[e]);(b=b.firstChild?b:b.nTr)&&(b=b.getAttribute("id"))&&da(a.rowId)(d,b);return{data:d,cells:f}}function Za(a,b,c,d){var f=a.aoData[b],e=f._aData,g=[],h,l;if(null===f.nTr){var n=c||z.createElement("tr");f.nTr=n;f.anCells=g;n._DT_RowIndex=b;cb(a,f);var m=0;for(h=a.aoColumns.length;m<h;m++){var p=a.aoColumns[m];var t=(l=c?!1:!0)?
|
||||
z.createElement(p.sCellType):d[m];t._DT_CellIndex={row:b,column:m};g.push(t);if(l||!(c&&!p.mRender&&p.mData===m||k.isPlainObject(p.mData)&&p.mData._===m+".display"))t.innerHTML=S(a,b,m,"display");p.sClass&&(t.className+=" "+p.sClass);p.bVisible&&!c?n.appendChild(t):!p.bVisible&&c&&t.parentNode.removeChild(t);p.fnCreatedCell&&p.fnCreatedCell.call(a.oInstance,t,S(a,b,m),e,b,m)}I(a,"aoRowCreatedCallback",null,[n,e,b,g])}f.nTr.setAttribute("role","row")}function cb(a,b){var c=b.nTr,d=b._aData;if(c){if(a=
|
||||
a.rowIdFn(d))c.id=a;d.DT_RowClass&&(a=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?Ja(b.__rowc.concat(a)):a,k(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&k(c).attr(d.DT_RowAttr);d.DT_RowData&&k(c).data(d.DT_RowData)}}function Eb(a){var b,c,d=a.nTHead,f=a.nTFoot,e=0===k("th, td",d).length,g=a.oClasses,h=a.aoColumns;e&&(c=k("<tr/>").appendTo(d));var l=0;for(b=h.length;l<b;l++){var n=h[l];var m=k(n.nTh).addClass(n.sClass);e&&m.appendTo(c);a.oFeatures.bSort&&(m.addClass(n.sSortingClass),
|
||||
!1!==n.bSortable&&(m.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),db(a,n.nTh,l)));n.sTitle!=m[0].innerHTML&&m.html(n.sTitle);eb(a,"header")(a,m,n,g)}e&&wa(a.aoHeader,d);k(d).children("tr").attr("role","row");k(d).children("tr").children("th, td").addClass(g.sHeaderTH);k(f).children("tr").children("th, td").addClass(g.sFooterTH);if(null!==f)for(a=a.aoFooter[0],l=0,b=a.length;l<b;l++)n=h[l],n.nTf=a[l].cell,n.sClass&&k(n.nTf).addClass(n.sClass)}function xa(a,b,c){var d,f,e=[],g=[],h=
|
||||
a.aoColumns.length;if(b){c===q&&(c=!1);var l=0;for(d=b.length;l<d;l++){e[l]=b[l].slice();e[l].nTr=b[l].nTr;for(f=h-1;0<=f;f--)a.aoColumns[f].bVisible||c||e[l].splice(f,1);g.push([])}l=0;for(d=e.length;l<d;l++){if(a=e[l].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=e[l].length;f<b;f++){var n=h=1;if(g[l][f]===q){a.appendChild(e[l][f].cell);for(g[l][f]=1;e[l+h]!==q&&e[l][f].cell==e[l+h][f].cell;)g[l+h][f]=1,h++;for(;e[l][f+n]!==q&&e[l][f].cell==e[l][f+n].cell;){for(c=0;c<h;c++)g[l+c][f+n]=1;n++}k(e[l][f].cell).attr("rowspan",
|
||||
h).attr("colspan",n)}}}}}function fa(a){var b=I(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==k.inArray(!1,b))U(a,!1);else{b=[];var c=0,d=a.asStripeClasses,f=d.length,e=a.oLanguage,g=a.iInitDisplayStart,h="ssp"==P(a),l=a.aiDisplay;a.bDrawing=!0;g!==q&&-1!==g&&(a._iDisplayStart=h?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);g=a._iDisplayStart;var n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,U(a,!1);else if(!h)a.iDraw++;else if(!a.bDestroying&&!Fb(a))return;if(0!==l.length)for(e=
|
||||
h?a.aoData.length:n,h=h?0:g;h<e;h++){var m=l[h],p=a.aoData[m];null===p.nTr&&Za(a,m);var t=p.nTr;if(0!==f){var v=d[c%f];p._sRowStripe!=v&&(k(t).removeClass(p._sRowStripe).addClass(v),p._sRowStripe=v)}I(a,"aoRowCallback",null,[t,p._aData,c,h,m]);b.push(t);c++}else c=e.sZeroRecords,1==a.iDraw&&"ajax"==P(a)?c=e.sLoadingRecords:e.sEmptyTable&&0===a.fnRecordsTotal()&&(c=e.sEmptyTable),b[0]=k("<tr/>",{"class":f?d[0]:""}).append(k("<td />",{valign:"top",colSpan:na(a),"class":a.oClasses.sRowEmpty}).html(c))[0];
|
||||
I(a,"aoHeaderCallback","header",[k(a.nTHead).children("tr")[0],bb(a),g,n,l]);I(a,"aoFooterCallback","footer",[k(a.nTFoot).children("tr")[0],bb(a),g,n,l]);d=k(a.nTBody);d.children().detach();d.append(k(b));I(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function ja(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&Gb(a);d?ya(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;fa(a);a._drawHold=!1}function Hb(a){var b=a.oClasses,
|
||||
c=k(a.nTable);c=k("<div/>").insertBefore(c);var d=a.oFeatures,f=k("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=f[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var e=a.sDom.split(""),g,h,l,n,m,p,t=0;t<e.length;t++){g=null;h=e[t];if("<"==h){l=k("<div/>")[0];n=e[t+1];if("'"==n||'"'==n){m="";for(p=2;e[t+p]!=n;)m+=e[t+p],p++;"H"==m?m=b.sJUIHeader:"F"==m&&(m=b.sJUIFooter);-1!=m.indexOf(".")?(n=m.split("."),l.id=n[0].substr(1,
|
||||
n[0].length-1),l.className=n[1]):"#"==m.charAt(0)?l.id=m.substr(1,m.length-1):l.className=m;t+=p}f.append(l);f=k(l)}else if(">"==h)f=f.parent();else if("l"==h&&d.bPaginate&&d.bLengthChange)g=Ib(a);else if("f"==h&&d.bFilter)g=Jb(a);else if("r"==h&&d.bProcessing)g=Kb(a);else if("t"==h)g=Lb(a);else if("i"==h&&d.bInfo)g=Mb(a);else if("p"==h&&d.bPaginate)g=Nb(a);else if(0!==u.ext.feature.length)for(l=u.ext.feature,p=0,n=l.length;p<n;p++)if(h==l[p].cFeature){g=l[p].fnInit(a);break}g&&(l=a.aanFeatures,l[h]||
|
||||
(l[h]=[]),l[h].push(g),f.append(g))}c.replaceWith(f);a.nHolding=null}function wa(a,b){b=k(b).children("tr");var c,d,f;a.splice(0,a.length);var e=0;for(f=b.length;e<f;e++)a.push([]);e=0;for(f=b.length;e<f;e++){var g=b[e];for(c=g.firstChild;c;){if("TD"==c.nodeName.toUpperCase()||"TH"==c.nodeName.toUpperCase()){var h=1*c.getAttribute("colspan");var l=1*c.getAttribute("rowspan");h=h&&0!==h&&1!==h?h:1;l=l&&0!==l&&1!==l?l:1;var n=0;for(d=a[e];d[n];)n++;var m=n;var p=1===h?!0:!1;for(d=0;d<h;d++)for(n=0;n<
|
||||
l;n++)a[e+n][m+d]={cell:c,unique:p},a[e+n].nTr=g}c=c.nextSibling}}}function Ka(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],wa(c,b)));b=0;for(var f=c.length;b<f;b++)for(var e=0,g=c[b].length;e<g;e++)!c[b][e].unique||d[e]&&a.bSortCellsTop||(d[e]=c[b][e].cell);return d}function La(a,b,c){I(a,"aoServerParams","serverParams",[b]);if(b&&Array.isArray(b)){var d={},f=/(.*?)\[\]$/;k.each(b,function(m,p){(m=p.name.match(f))?(m=m[0],d[m]||(d[m]=[]),d[m].push(p.value)):d[p.name]=p.value});b=d}var e=a.ajax,g=a.oInstance,
|
||||
h=function(m){I(a,null,"xhr",[a,m,a.jqXHR]);c(m)};if(k.isPlainObject(e)&&e.data){var l=e.data;var n="function"===typeof l?l(b,a):l;b="function"===typeof l&&n?n:k.extend(!0,b,n);delete e.data}n={data:b,success:function(m){var p=m.error||m.sError;p&&aa(a,0,p);a.json=m;h(m)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(m,p,t){t=I(a,null,"xhr",[a,null,a.jqXHR]);-1===k.inArray(!0,t)&&("parsererror"==p?aa(a,0,"Invalid JSON response",1):4===m.readyState&&aa(a,0,"Ajax error",7));U(a,!1)}};
|
||||
a.oAjaxData=b;I(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(g,a.sAjaxSource,k.map(b,function(m,p){return{name:p,value:m}}),h,a):a.sAjaxSource||"string"===typeof e?a.jqXHR=k.ajax(k.extend(n,{url:e||a.sAjaxSource})):"function"===typeof e?a.jqXHR=e.call(g,b,h,a):(a.jqXHR=k.ajax(k.extend(n,e)),e.data=l)}function Fb(a){return a.bAjaxDataGet?(a.iDraw++,U(a,!0),La(a,Ob(a),function(b){Pb(a,b)}),!1):!0}function Ob(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,f=a.oPreviousSearch,e=a.aoPreSearchCols,
|
||||
g=[],h=pa(a);var l=a._iDisplayStart;var n=!1!==d.bPaginate?a._iDisplayLength:-1;var m=function(x,r){g.push({name:x,value:r})};m("sEcho",a.iDraw);m("iColumns",c);m("sColumns",T(b,"sName").join(","));m("iDisplayStart",l);m("iDisplayLength",n);var p={draw:a.iDraw,columns:[],order:[],start:l,length:n,search:{value:f.sSearch,regex:f.bRegex}};for(l=0;l<c;l++){var t=b[l];var v=e[l];n="function"==typeof t.mData?"function":t.mData;p.columns.push({data:n,name:t.sName,searchable:t.bSearchable,orderable:t.bSortable,
|
||||
search:{value:v.sSearch,regex:v.bRegex}});m("mDataProp_"+l,n);d.bFilter&&(m("sSearch_"+l,v.sSearch),m("bRegex_"+l,v.bRegex),m("bSearchable_"+l,t.bSearchable));d.bSort&&m("bSortable_"+l,t.bSortable)}d.bFilter&&(m("sSearch",f.sSearch),m("bRegex",f.bRegex));d.bSort&&(k.each(h,function(x,r){p.order.push({column:r.col,dir:r.dir});m("iSortCol_"+x,r.col);m("sSortDir_"+x,r.dir)}),m("iSortingCols",h.length));b=u.ext.legacy.ajax;return null===b?a.sAjaxSource?g:p:b?g:p}function Pb(a,b){var c=function(g,h){return b[g]!==
|
||||
q?b[g]:b[h]},d=Ma(a,b),f=c("sEcho","draw"),e=c("iTotalRecords","recordsTotal");c=c("iTotalDisplayRecords","recordsFiltered");if(f!==q){if(1*f<a.iDraw)return;a.iDraw=1*f}Ha(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(c,10);f=0;for(e=d.length;f<e;f++)ea(a,d[f]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;fa(a);a._bInitComplete||Na(a,b);a.bAjaxDataGet=!0;U(a,!1)}function Ma(a,b){a=k.isPlainObject(a.ajax)&&a.ajax.dataSrc!==q?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===
|
||||
a?b.aaData||b[a]:""!==a?ia(a)(b):b}function Jb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,f=a.oPreviousSearch,e=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',h=d.sSearch;h=h.match(/_INPUT_/)?h.replace("_INPUT_",g):h+g;b=k("<div/>",{id:e.f?null:c+"_filter","class":b.sFilter}).append(k("<label/>").append(h));var l=function(){var m=this.value?this.value:"";m!=f.sSearch&&(ya(a,{sSearch:m,bRegex:f.bRegex,bSmart:f.bSmart,bCaseInsensitive:f.bCaseInsensitive}),a._iDisplayStart=0,
|
||||
fa(a))};e=null!==a.searchDelay?a.searchDelay:"ssp"===P(a)?400:0;var n=k("input",b).val(f.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",e?fb(l,e):l).on("mouseup",function(m){setTimeout(function(){l.call(n[0])},10)}).on("keypress.DT",function(m){if(13==m.keyCode)return!1}).attr("aria-controls",c);k(a.nTable).on("search.dt.DT",function(m,p){if(a===p)try{n[0]!==z.activeElement&&n.val(f.sSearch)}catch(t){}});return b[0]}function ya(a,b,c){var d=a.oPreviousSearch,
|
||||
f=a.aoPreSearchCols,e=function(h){d.sSearch=h.sSearch;d.bRegex=h.bRegex;d.bSmart=h.bSmart;d.bCaseInsensitive=h.bCaseInsensitive},g=function(h){return h.bEscapeRegex!==q?!h.bEscapeRegex:h.bRegex};Ya(a);if("ssp"!=P(a)){Qb(a,b.sSearch,c,g(b),b.bSmart,b.bCaseInsensitive);e(b);for(b=0;b<f.length;b++)Rb(a,f[b].sSearch,b,g(f[b]),f[b].bSmart,f[b].bCaseInsensitive);Sb(a)}else e(b);a.bFiltered=!0;I(a,null,"search",[a])}function Sb(a){for(var b=u.ext.search,c=a.aiDisplay,d,f,e=0,g=b.length;e<g;e++){for(var h=
|
||||
[],l=0,n=c.length;l<n;l++)f=c[l],d=a.aoData[f],b[e](a,d._aFilterData,f,d._aData,l)&&h.push(f);c.length=0;k.merge(c,h)}}function Rb(a,b,c,d,f,e){if(""!==b){var g=[],h=a.aiDisplay;d=gb(b,d,f,e);for(f=0;f<h.length;f++)b=a.aoData[h[f]]._aFilterData[c],d.test(b)&&g.push(h[f]);a.aiDisplay=g}}function Qb(a,b,c,d,f,e){f=gb(b,d,f,e);var g=a.oPreviousSearch.sSearch,h=a.aiDisplayMaster;e=[];0!==u.ext.search.length&&(c=!0);var l=Tb(a);if(0>=b.length)a.aiDisplay=h.slice();else{if(l||c||d||g.length>b.length||0!==
|
||||
b.indexOf(g)||a.bSorted)a.aiDisplay=h.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)f.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function gb(a,b,c,d){a=b?a:hb(a);c&&(a="^(?=.*?"+k.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(f){if('"'===f.charAt(0)){var e=f.match(/^"(.*)"$/);f=e?e[1]:f}return f.replace('"',"")}).join(")(?=.*?")+").*$");return new RegExp(a,d?"i":"")}function Tb(a){var b=a.aoColumns,c,d,f=u.ext.type.search;var e=!1;var g=0;for(c=a.aoData.length;g<c;g++){var h=a.aoData[g];
|
||||
if(!h._aFilterData){var l=[];var n=0;for(d=b.length;n<d;n++){e=b[n];if(e.bSearchable){var m=S(a,g,n,"filter");f[e.sType]&&(m=f[e.sType](m));null===m&&(m="");"string"!==typeof m&&m.toString&&(m=m.toString())}else m="";m.indexOf&&-1!==m.indexOf("&")&&(Oa.innerHTML=m,m=rc?Oa.textContent:Oa.innerText);m.replace&&(m=m.replace(/[\r\n\u2028]/g,""));l.push(m)}h._aFilterData=l;h._sFilterRow=l.join(" ");e=!0}}return e}function Ub(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,caseInsensitive:a.bCaseInsensitive}}
|
||||
function Vb(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function Mb(a){var b=a.sTableId,c=a.aanFeatures.i,d=k("<div/>",{"class":a.oClasses.sInfo,id:c?null:b+"_info"});c||(a.aoDrawCallback.push({fn:Wb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),k(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Wb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,f=a.fnDisplayEnd(),e=a.fnRecordsTotal(),
|
||||
g=a.fnRecordsDisplay(),h=g?c.sInfo:c.sInfoEmpty;g!==e&&(h+=" "+c.sInfoFiltered);h+=c.sInfoPostFix;h=Xb(a,h);c=c.fnInfoCallback;null!==c&&(h=c.call(a.oInstance,a,d,f,e,g,h));k(b).html(h)}}function Xb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,f=a._iDisplayLength,e=a.fnRecordsDisplay(),g=-1===f;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,e)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/
|
||||
f))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(e/f)))}function za(a){var b=a.iInitDisplayStart,c=a.aoColumns;var d=a.oFeatures;var f=a.bDeferLoading;if(a.bInitialised){Hb(a);Eb(a);xa(a,a.aoHeader);xa(a,a.aoFooter);U(a,!0);d.bAutoWidth&&Xa(a);var e=0;for(d=c.length;e<d;e++){var g=c[e];g.sWidth&&(g.nTh.style.width=K(g.sWidth))}I(a,null,"preInit",[a]);ja(a);c=P(a);if("ssp"!=c||f)"ajax"==c?La(a,[],function(h){var l=Ma(a,h);for(e=0;e<l.length;e++)ea(a,l[e]);a.iInitDisplayStart=b;ja(a);U(a,!1);Na(a,h)},
|
||||
a):(U(a,!1),Na(a))}else setTimeout(function(){za(a)},200)}function Na(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&ra(a);I(a,null,"plugin-init",[a,b]);I(a,"aoInitComplete","init",[a,b])}function ib(a,b){b=parseInt(b,10);a._iDisplayLength=b;jb(a);I(a,null,"length",[a,b])}function Ib(a){var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,f=Array.isArray(d[0]),e=f?d[0]:d;d=f?d[1]:d;f=k("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect});for(var g=0,h=e.length;g<h;g++)f[0][g]=new Option("number"===
|
||||
typeof d[g]?a.fnFormatNumber(d[g]):d[g],e[g]);var l=k("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(l[0].id=c+"_length");l.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",f[0].outerHTML));k("select",l).val(a._iDisplayLength).on("change.DT",function(n){ib(a,k(this).val());fa(a)});k(a.nTable).on("length.dt.DT",function(n,m,p){a===m&&k("select",l).val(p)});return l[0]}function Nb(a){var b=a.sPaginationType,c=u.ext.pager[b],d="function"===typeof c,f=function(g){fa(g)};b=k("<div/>").addClass(a.oClasses.sPaging+
|
||||
b)[0];var e=a.aanFeatures;d||c.fnInit(a,b,f);e.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(g){if(d){var h=g._iDisplayStart,l=g._iDisplayLength,n=g.fnRecordsDisplay(),m=-1===l;h=m?0:Math.ceil(h/l);l=m?1:Math.ceil(n/l);n=c(h,l);var p;m=0;for(p=e.p.length;m<p;m++)eb(g,"pageButton")(g,e.p[m],m,n,h,l)}else c.fnUpdate(g,f)},sName:"pagination"}));return b}function kb(a,b,c){var d=a._iDisplayStart,f=a._iDisplayLength,e=a.fnRecordsDisplay();0===e||-1===f?d=0:"number"===typeof b?(d=b*
|
||||
f,d>e&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=f?d-f:0,0>d&&(d=0)):"next"==b?d+f<e&&(d+=f):"last"==b?d=Math.floor((e-1)/f)*f:aa(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(I(a,null,"page",[a]),c&&fa(a));return b}function Kb(a){return k("<div/>",{id:a.aanFeatures.r?null:a.sTableId+"_processing","class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function U(a,b){a.oFeatures.bProcessing&&k(a.aanFeatures.r).css("display",b?"block":
|
||||
"none");I(a,null,"processing",[a,b])}function Lb(a){var b=k(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,f=c.sY,e=a.oClasses,g=b.children("caption"),h=g.length?g[0]._captionSide:null,l=k(b[0].cloneNode(!1)),n=k(b[0].cloneNode(!1)),m=b.children("tfoot");m.length||(m=null);l=k("<div/>",{"class":e.sScrollWrapper}).append(k("<div/>",{"class":e.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?d?K(d):null:"100%"}).append(k("<div/>",
|
||||
{"class":e.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(l.removeAttr("id").css("margin-left",0).append("top"===h?g:null).append(b.children("thead"))))).append(k("<div/>",{"class":e.sScrollBody}).css({position:"relative",overflow:"auto",width:d?K(d):null}).append(b));m&&l.append(k("<div/>",{"class":e.sScrollFoot}).css({overflow:"hidden",border:0,width:d?d?K(d):null:"100%"}).append(k("<div/>",{"class":e.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
|
||||
0).append("bottom"===h?g:null).append(b.children("tfoot")))));b=l.children();var p=b[0];e=b[1];var t=m?b[2]:null;if(d)k(e).on("scroll.DT",function(v){v=this.scrollLeft;p.scrollLeft=v;m&&(t.scrollLeft=v)});k(e).css("max-height",f);c.bCollapse||k(e).css("height",f);a.nScrollHead=p;a.nScrollBody=e;a.nScrollFoot=t;a.aoDrawCallback.push({fn:Ea,sName:"scrolling"});return l[0]}function Ea(a){var b=a.oScroll,c=b.sX,d=b.sXInner,f=b.sY;b=b.iBarWidth;var e=k(a.nScrollHead),g=e[0].style,h=e.children("div"),l=
|
||||
h[0].style,n=h.children("table");h=a.nScrollBody;var m=k(h),p=h.style,t=k(a.nScrollFoot).children("div"),v=t.children("table"),x=k(a.nTHead),r=k(a.nTable),A=r[0],E=A.style,H=a.nTFoot?k(a.nTFoot):null,W=a.oBrowser,M=W.bScrollOversize,C=T(a.aoColumns,"nTh"),B=[],ba=[],X=[],lb=[],Aa,Yb=function(F){F=F.style;F.paddingTop="0";F.paddingBottom="0";F.borderTopWidth="0";F.borderBottomWidth="0";F.height=0};var ha=h.scrollHeight>h.clientHeight;if(a.scrollBarVis!==ha&&a.scrollBarVis!==q)a.scrollBarVis=ha,ra(a);
|
||||
else{a.scrollBarVis=ha;r.children("thead, tfoot").remove();if(H){var ka=H.clone().prependTo(r);var la=H.find("tr");ka=ka.find("tr")}var mb=x.clone().prependTo(r);x=x.find("tr");ha=mb.find("tr");mb.find("th, td").removeAttr("tabindex");c||(p.width="100%",e[0].style.width="100%");k.each(Ka(a,mb),function(F,Y){Aa=sa(a,F);Y.style.width=a.aoColumns[Aa].sWidth});H&&Z(function(F){F.style.width=""},ka);e=r.outerWidth();""===c?(E.width="100%",M&&(r.find("tbody").height()>h.offsetHeight||"scroll"==m.css("overflow-y"))&&
|
||||
(E.width=K(r.outerWidth()-b)),e=r.outerWidth()):""!==d&&(E.width=K(d),e=r.outerWidth());Z(Yb,ha);Z(function(F){X.push(F.innerHTML);B.push(K(k(F).css("width")))},ha);Z(function(F,Y){-1!==k.inArray(F,C)&&(F.style.width=B[Y])},x);k(ha).height(0);H&&(Z(Yb,ka),Z(function(F){lb.push(F.innerHTML);ba.push(K(k(F).css("width")))},ka),Z(function(F,Y){F.style.width=ba[Y]},la),k(ka).height(0));Z(function(F,Y){F.innerHTML='<div class="dataTables_sizing">'+X[Y]+"</div>";F.childNodes[0].style.height="0";F.childNodes[0].style.overflow=
|
||||
"hidden";F.style.width=B[Y]},ha);H&&Z(function(F,Y){F.innerHTML='<div class="dataTables_sizing">'+lb[Y]+"</div>";F.childNodes[0].style.height="0";F.childNodes[0].style.overflow="hidden";F.style.width=ba[Y]},ka);r.outerWidth()<e?(la=h.scrollHeight>h.offsetHeight||"scroll"==m.css("overflow-y")?e+b:e,M&&(h.scrollHeight>h.offsetHeight||"scroll"==m.css("overflow-y"))&&(E.width=K(la-b)),""!==c&&""===d||aa(a,1,"Possible column misalignment",6)):la="100%";p.width=K(la);g.width=K(la);H&&(a.nScrollFoot.style.width=
|
||||
K(la));!f&&M&&(p.height=K(A.offsetHeight+b));c=r.outerWidth();n[0].style.width=K(c);l.width=K(c);d=r.height()>h.clientHeight||"scroll"==m.css("overflow-y");f="padding"+(W.bScrollbarLeft?"Left":"Right");l[f]=d?b+"px":"0px";H&&(v[0].style.width=K(c),t[0].style.width=K(c),t[0].style[f]=d?b+"px":"0px");r.children("colgroup").insertBefore(r.children("thead"));m.trigger("scroll");!a.bSorted&&!a.bFiltered||a._drawHold||(h.scrollTop=0)}}function Z(a,b,c){for(var d=0,f=0,e=b.length,g,h;f<e;){g=b[f].firstChild;
|
||||
for(h=c?c[f].firstChild:null;g;)1===g.nodeType&&(c?a(g,h,d):a(g,d),d++),g=g.nextSibling,h=c?h.nextSibling:null;f++}}function Xa(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,f=d.sY,e=d.sX,g=d.sXInner,h=c.length,l=Fa(a,"bVisible"),n=k("th",a.nTHead),m=b.getAttribute("width"),p=b.parentNode,t=!1,v,x=a.oBrowser;d=x.bScrollOversize;(v=b.style.width)&&-1!==v.indexOf("%")&&(m=v);for(v=0;v<l.length;v++){var r=c[l[v]];null!==r.sWidth&&(r.sWidth=Zb(r.sWidthOrig,p),t=!0)}if(d||!t&&!e&&!f&&h==na(a)&&h==n.length)for(v=
|
||||
0;v<h;v++)l=sa(a,v),null!==l&&(c[l].sWidth=K(n.eq(v).width()));else{h=k(b).clone().css("visibility","hidden").removeAttr("id");h.find("tbody tr").remove();var A=k("<tr/>").appendTo(h.find("tbody"));h.find("thead, tfoot").remove();h.append(k(a.nTHead).clone()).append(k(a.nTFoot).clone());h.find("tfoot th, tfoot td").css("width","");n=Ka(a,h.find("thead")[0]);for(v=0;v<l.length;v++)r=c[l[v]],n[v].style.width=null!==r.sWidthOrig&&""!==r.sWidthOrig?K(r.sWidthOrig):"",r.sWidthOrig&&e&&k(n[v]).append(k("<div/>").css({width:r.sWidthOrig,
|
||||
margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(v=0;v<l.length;v++)t=l[v],r=c[t],k($b(a,t)).clone(!1).append(r.sContentPadding).appendTo(A);k("[name]",h).removeAttr("name");r=k("<div/>").css(e||f?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(h).appendTo(p);e&&g?h.width(g):e?(h.css("width","auto"),h.removeAttr("width"),h.width()<p.clientWidth&&m&&h.width(p.clientWidth)):f?h.width(p.clientWidth):m&&h.width(m);for(v=f=0;v<l.length;v++)p=k(n[v]),g=p.outerWidth()-
|
||||
p.width(),p=x.bBounding?Math.ceil(n[v].getBoundingClientRect().width):p.outerWidth(),f+=p,c[l[v]].sWidth=K(p-g);b.style.width=K(f);r.remove()}m&&(b.style.width=K(m));!m&&!e||a._reszEvt||(b=function(){k(y).on("resize.DT-"+a.sInstance,fb(function(){ra(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0)}function Zb(a,b){if(!a)return 0;a=k("<div/>").css("width",K(a)).appendTo(b||z.body);b=a[0].offsetWidth;a.remove();return b}function $b(a,b){var c=ac(a,b);if(0>c)return null;var d=a.aoData[c];return d.nTr?d.anCells[b]:
|
||||
k("<td/>").html(S(a,c,b,"display"))[0]}function ac(a,b){for(var c,d=-1,f=-1,e=0,g=a.aoData.length;e<g;e++)c=S(a,e,b,"display")+"",c=c.replace(sc,""),c=c.replace(/ /g," "),c.length>d&&(d=c.length,f=e);return f}function K(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function pa(a){var b=[],c=a.aoColumns;var d=a.aaSortingFixed;var f=k.isPlainObject(d);var e=[];var g=function(m){m.length&&!Array.isArray(m[0])?e.push(m):k.merge(e,m)};Array.isArray(d)&&g(d);
|
||||
f&&d.pre&&g(d.pre);g(a.aaSorting);f&&d.post&&g(d.post);for(a=0;a<e.length;a++){var h=e[a][0];g=c[h].aDataSort;d=0;for(f=g.length;d<f;d++){var l=g[d];var n=c[l].sType||"string";e[a]._idx===q&&(e[a]._idx=k.inArray(e[a][1],c[l].asSorting));b.push({src:h,col:l,dir:e[a][1],index:e[a]._idx,type:n,formatter:u.ext.type.order[n+"-pre"]})}}return b}function Gb(a){var b,c=[],d=u.ext.type.order,f=a.aoData,e=0,g=a.aiDisplayMaster;Ya(a);var h=pa(a);var l=0;for(b=h.length;l<b;l++){var n=h[l];n.formatter&&e++;bc(a,
|
||||
n.col)}if("ssp"!=P(a)&&0!==h.length){l=0;for(b=g.length;l<b;l++)c[g[l]]=l;e===h.length?g.sort(function(m,p){var t,v=h.length,x=f[m]._aSortData,r=f[p]._aSortData;for(t=0;t<v;t++){var A=h[t];var E=x[A.col];var H=r[A.col];E=E<H?-1:E>H?1:0;if(0!==E)return"asc"===A.dir?E:-E}E=c[m];H=c[p];return E<H?-1:E>H?1:0}):g.sort(function(m,p){var t,v=h.length,x=f[m]._aSortData,r=f[p]._aSortData;for(t=0;t<v;t++){var A=h[t];var E=x[A.col];var H=r[A.col];A=d[A.type+"-"+A.dir]||d["string-"+A.dir];E=A(E,H);if(0!==E)return E}E=
|
||||
c[m];H=c[p];return E<H?-1:E>H?1:0})}a.bSorted=!0}function cc(a){var b=a.aoColumns,c=pa(a);a=a.oLanguage.oAria;for(var d=0,f=b.length;d<f;d++){var e=b[d];var g=e.asSorting;var h=e.sTitle.replace(/<.*?>/g,"");var l=e.nTh;l.removeAttribute("aria-sort");e.bSortable&&(0<c.length&&c[0].col==d?(l.setAttribute("aria-sort","asc"==c[0].dir?"ascending":"descending"),e=g[c[0].index+1]||g[0]):e=g[0],h+="asc"===e?a.sSortAscending:a.sSortDescending);l.setAttribute("aria-label",h)}}function nb(a,b,c,d){var f=a.aaSorting,
|
||||
e=a.aoColumns[b].asSorting,g=function(h,l){var n=h._idx;n===q&&(n=k.inArray(h[1],e));return n+1<e.length?n+1:l?null:0};"number"===typeof f[0]&&(f=a.aaSorting=[f]);c&&a.oFeatures.bSortMulti?(c=k.inArray(b,T(f,"0")),-1!==c?(b=g(f[c],!0),null===b&&1===f.length&&(b=0),null===b?f.splice(c,1):(f[c][1]=e[b],f[c]._idx=b)):(f.push([b,e[0],0]),f[f.length-1]._idx=0)):f.length&&f[0][0]==b?(b=g(f[0]),f.length=1,f[0][1]=e[b],f[0]._idx=b):(f.length=0,f.push([b,e[0]]),f[0]._idx=0);ja(a);"function"==typeof d&&d(a)}
|
||||
function db(a,b,c,d){var f=a.aoColumns[c];ob(b,{},function(e){!1!==f.bSortable&&(a.oFeatures.bProcessing?(U(a,!0),setTimeout(function(){nb(a,c,e.shiftKey,d);"ssp"!==P(a)&&U(a,!1)},0)):nb(a,c,e.shiftKey,d))})}function Pa(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=pa(a),f=a.oFeatures,e;if(f.bSort&&f.bSortClasses){f=0;for(e=b.length;f<e;f++){var g=b[f].src;k(T(a.aoData,"anCells",g)).removeClass(c+(2>f?f+1:3))}f=0;for(e=d.length;f<e;f++)g=d[f].src,k(T(a.aoData,"anCells",g)).addClass(c+(2>f?f+1:3))}a.aLastSort=
|
||||
d}function bc(a,b){var c=a.aoColumns[b],d=u.ext.order[c.sSortDataType],f;d&&(f=d.call(a.oInstance,a,b,ta(a,b)));for(var e,g=u.ext.type.order[c.sType+"-pre"],h=0,l=a.aoData.length;h<l;h++)if(c=a.aoData[h],c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)e=d?f[h]:S(a,h,b,"sort"),c._aSortData[b]=g?g(e):e}function Qa(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:k.extend(!0,[],a.aaSorting),search:Ub(a.oPreviousSearch),columns:k.map(a.aoColumns,
|
||||
function(c,d){return{visible:c.bVisible,search:Ub(a.aoPreSearchCols[d])}})};I(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,b)}}function dc(a,b,c){var d,f,e=a.aoColumns;b=function(h){if(h&&h.time){var l=I(a,"aoStateLoadParams","stateLoadParams",[a,h]);if(-1===k.inArray(!1,l)&&(l=a.iStateDuration,!(0<l&&h.time<+new Date-1E3*l||h.columns&&e.length!==h.columns.length))){a.oLoadedState=k.extend(!0,{},h);h.start!==q&&(a._iDisplayStart=h.start,a.iInitDisplayStart=
|
||||
h.start);h.length!==q&&(a._iDisplayLength=h.length);h.order!==q&&(a.aaSorting=[],k.each(h.order,function(n,m){a.aaSorting.push(m[0]>=e.length?[0,m[1]]:m)}));h.search!==q&&k.extend(a.oPreviousSearch,Vb(h.search));if(h.columns)for(d=0,f=h.columns.length;d<f;d++)l=h.columns[d],l.visible!==q&&(e[d].bVisible=l.visible),l.search!==q&&k.extend(a.aoPreSearchCols[d],Vb(l.search));I(a,"aoStateLoaded","stateLoaded",[a,h])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==
|
||||
q&&b(g)}else c()}function Ra(a){var b=u.settings;a=k.inArray(a,T(b,"nTable"));return-1!==a?b[a]:null}function aa(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)y.console&&console.log&&console.log(c);else if(b=u.ext,b=b.sErrMode||b.errMode,a&&I(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function V(a,b,c,d){Array.isArray(c)?
|
||||
k.each(c,function(f,e){Array.isArray(e)?V(a,b,e[0],e[1]):V(a,b,e)}):(d===q&&(d=c),b[c]!==q&&(a[d]=b[c]))}function pb(a,b,c){var d;for(d in b)if(b.hasOwnProperty(d)){var f=b[d];k.isPlainObject(f)?(k.isPlainObject(a[d])||(a[d]={}),k.extend(!0,a[d],f)):c&&"data"!==d&&"aaData"!==d&&Array.isArray(f)?a[d]=f.slice():a[d]=f}return a}function ob(a,b,c){k(a).on("click.DT",b,function(d){k(a).trigger("blur");c(d)}).on("keypress.DT",b,function(d){13===d.which&&(d.preventDefault(),c(d))}).on("selectstart.DT",function(){return!1})}
|
||||
function Q(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function I(a,b,c,d){var f=[];b&&(f=k.map(a[b].slice().reverse(),function(e,g){return e.fn.apply(a.oInstance,d)}));null!==c&&(b=k.Event(c+".dt"),k(a.nTable).trigger(b,d),f.push(b.result));return f}function jb(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function eb(a,b){a=a.renderer;var c=u.ext.renderer[b];return k.isPlainObject(a)&&a[b]?c[a[b]]||c._:"string"===typeof a?c[a]||
|
||||
c._:c._}function P(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Ba(a,b){var c=ec.numbers_length,d=Math.floor(c/2);b<=c?a=qa(0,b):a<=d?(a=qa(0,c-2),a.push("ellipsis"),a.push(b-1)):(a>=b-1-d?a=qa(b-(c-2),b):(a=qa(a-d+2,a+d-1),a.push("ellipsis"),a.push(b-1)),a.splice(0,0,"ellipsis"),a.splice(0,0,0));a.DT_el="span";return a}function Va(a){k.each({num:function(b){return Sa(b,a)},"num-fmt":function(b){return Sa(b,a,qb)},"html-num":function(b){return Sa(b,a,Ta)},"html-num-fmt":function(b){return Sa(b,
|
||||
a,Ta,qb)}},function(b,c){L.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(L.type.search[b+a]=L.type.search.html)})}function fc(a){return function(){var b=[Ra(this[u.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return u.ext.internal[a].apply(this,b)}}var u=function(a){this.$=function(e,g){return this.api(!0).$(e,g)};this._=function(e,g){return this.api(!0).rows(e,g).data()};this.api=function(e){return e?new D(Ra(this[L.iApiIndex])):new D(this)};this.fnAddData=function(e,g){var h=this.api(!0);
|
||||
e=Array.isArray(e)&&(Array.isArray(e[0])||k.isPlainObject(e[0]))?h.rows.add(e):h.row.add(e);(g===q||g)&&h.draw();return e.flatten().toArray()};this.fnAdjustColumnSizing=function(e){var g=this.api(!0).columns.adjust(),h=g.settings()[0],l=h.oScroll;e===q||e?g.draw(!1):(""!==l.sX||""!==l.sY)&&Ea(h)};this.fnClearTable=function(e){var g=this.api(!0).clear();(e===q||e)&&g.draw()};this.fnClose=function(e){this.api(!0).row(e).child.hide()};this.fnDeleteRow=function(e,g,h){var l=this.api(!0);e=l.rows(e);var n=
|
||||
e.settings()[0],m=n.aoData[e[0][0]];e.remove();g&&g.call(this,n,m);(h===q||h)&&l.draw();return m};this.fnDestroy=function(e){this.api(!0).destroy(e)};this.fnDraw=function(e){this.api(!0).draw(e)};this.fnFilter=function(e,g,h,l,n,m){n=this.api(!0);null===g||g===q?n.search(e,h,l,m):n.column(g).search(e,h,l,m);n.draw()};this.fnGetData=function(e,g){var h=this.api(!0);if(e!==q){var l=e.nodeName?e.nodeName.toLowerCase():"";return g!==q||"td"==l||"th"==l?h.cell(e,g).data():h.row(e).data()||null}return h.data().toArray()};
|
||||
this.fnGetNodes=function(e){var g=this.api(!0);return e!==q?g.row(e).node():g.rows().nodes().flatten().toArray()};this.fnGetPosition=function(e){var g=this.api(!0),h=e.nodeName.toUpperCase();return"TR"==h?g.row(e).index():"TD"==h||"TH"==h?(e=g.cell(e).index(),[e.row,e.columnVisible,e.column]):null};this.fnIsOpen=function(e){return this.api(!0).row(e).child.isShown()};this.fnOpen=function(e,g,h){return this.api(!0).row(e).child(g,h).show().child()[0]};this.fnPageChange=function(e,g){e=this.api(!0).page(e);
|
||||
(g===q||g)&&e.draw(!1)};this.fnSetColumnVis=function(e,g,h){e=this.api(!0).column(e).visible(g);(h===q||h)&&e.columns.adjust().draw()};this.fnSettings=function(){return Ra(this[L.iApiIndex])};this.fnSort=function(e){this.api(!0).order(e).draw()};this.fnSortListener=function(e,g,h){this.api(!0).order.listener(e,g,h)};this.fnUpdate=function(e,g,h,l,n){var m=this.api(!0);h===q||null===h?m.row(g).data(e):m.cell(g,h).data(e);(n===q||n)&&m.columns.adjust();(l===q||l)&&m.draw();return 0};this.fnVersionCheck=
|
||||
L.fnVersionCheck;var b=this,c=a===q,d=this.length;c&&(a={});this.oApi=this.internal=L.internal;for(var f in u.ext.internal)f&&(this[f]=fc(f));this.each(function(){var e={},g=1<d?pb(e,a,!0):a,h=0,l;e=this.getAttribute("id");var n=!1,m=u.defaults,p=k(this);if("table"!=this.nodeName.toLowerCase())aa(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{yb(m);zb(m.column);O(m,m,!0);O(m.column,m.column,!0);O(m,k.extend(g,p.data()),!0);var t=u.settings;h=0;for(l=t.length;h<l;h++){var v=t[h];
|
||||
if(v.nTable==this||v.nTHead&&v.nTHead.parentNode==this||v.nTFoot&&v.nTFoot.parentNode==this){var x=g.bRetrieve!==q?g.bRetrieve:m.bRetrieve;if(c||x)return v.oInstance;if(g.bDestroy!==q?g.bDestroy:m.bDestroy){v.oInstance.fnDestroy();break}else{aa(v,0,"Cannot reinitialise DataTable",3);return}}if(v.sTableId==this.id){t.splice(h,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+u.ext._unique++;var r=k.extend(!0,{},u.models.oSettings,{sDestroyWidth:p[0].style.width,sInstance:e,sTableId:e});r.nTable=
|
||||
this;r.oApi=b.internal;r.oInit=g;t.push(r);r.oInstance=1===b.length?b:p.dataTable();yb(g);ma(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=Array.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);g=pb(k.extend(!0,{},m),g);V(r.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));V(r,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed",
|
||||
"aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"]]);V(r.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);V(r.oLanguage,g,"fnInfoCallback");Q(r,"aoDrawCallback",g.fnDrawCallback,
|
||||
"user");Q(r,"aoServerParams",g.fnServerParams,"user");Q(r,"aoStateSaveParams",g.fnStateSaveParams,"user");Q(r,"aoStateLoadParams",g.fnStateLoadParams,"user");Q(r,"aoStateLoaded",g.fnStateLoaded,"user");Q(r,"aoRowCallback",g.fnRowCallback,"user");Q(r,"aoRowCreatedCallback",g.fnCreatedRow,"user");Q(r,"aoHeaderCallback",g.fnHeaderCallback,"user");Q(r,"aoFooterCallback",g.fnFooterCallback,"user");Q(r,"aoInitComplete",g.fnInitComplete,"user");Q(r,"aoPreDrawCallback",g.fnPreDrawCallback,"user");r.rowIdFn=
|
||||
ia(g.rowId);Ab(r);var A=r.oClasses;k.extend(A,u.ext.classes,g.oClasses);p.addClass(A.sTable);r.iInitDisplayStart===q&&(r.iInitDisplayStart=g.iDisplayStart,r._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(r.bDeferLoading=!0,e=Array.isArray(g.iDeferLoading),r._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,r._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var E=r.oLanguage;k.extend(!0,E,g.oLanguage);E.sUrl&&(k.ajax({dataType:"json",url:E.sUrl,success:function(C){ma(C);O(m.oLanguage,
|
||||
C);k.extend(!0,E,C);za(r)},error:function(){za(r)}}),n=!0);null===g.asStripeClasses&&(r.asStripeClasses=[A.sStripeOdd,A.sStripeEven]);e=r.asStripeClasses;var H=p.children("tbody").find("tr").eq(0);-1!==k.inArray(!0,k.map(e,function(C,B){return H.hasClass(C)}))&&(k("tbody tr",this).removeClass(e.join(" ")),r.asDestroyStripes=e.slice());e=[];t=this.getElementsByTagName("thead");0!==t.length&&(wa(r.aoHeader,t[0]),e=Ka(r));if(null===g.aoColumns)for(t=[],h=0,l=e.length;h<l;h++)t.push(null);else t=g.aoColumns;
|
||||
h=0;for(l=t.length;h<l;h++)Wa(r,e?e[h]:null);Cb(r,g.aoColumnDefs,t,function(C,B){Da(r,C,B)});if(H.length){var W=function(C,B){return null!==C.getAttribute("data-"+B)?B:null};k(H[0]).children("th, td").each(function(C,B){var ba=r.aoColumns[C];if(ba.mData===C){var X=W(B,"sort")||W(B,"order");B=W(B,"filter")||W(B,"search");if(null!==X||null!==B)ba.mData={_:C+".display",sort:null!==X?C+".@data-"+X:q,type:null!==X?C+".@data-"+X:q,filter:null!==B?C+".@data-"+B:q},Da(r,C)}})}var M=r.oFeatures;e=function(){if(g.aaSorting===
|
||||
q){var C=r.aaSorting;h=0;for(l=C.length;h<l;h++)C[h][1]=r.aoColumns[h].asSorting[0]}Pa(r);M.bSort&&Q(r,"aoDrawCallback",function(){if(r.bSorted){var ba=pa(r),X={};k.each(ba,function(lb,Aa){X[Aa.src]=Aa.dir});I(r,null,"order",[r,ba,X]);cc(r)}});Q(r,"aoDrawCallback",function(){(r.bSorted||"ssp"===P(r)||M.bDeferRender)&&Pa(r)},"sc");C=p.children("caption").each(function(){this._captionSide=k(this).css("caption-side")});var B=p.children("thead");0===B.length&&(B=k("<thead/>").appendTo(p));r.nTHead=B[0];
|
||||
B=p.children("tbody");0===B.length&&(B=k("<tbody/>").appendTo(p));r.nTBody=B[0];B=p.children("tfoot");0===B.length&&0<C.length&&(""!==r.oScroll.sX||""!==r.oScroll.sY)&&(B=k("<tfoot/>").appendTo(p));0===B.length||0===B.children().length?p.addClass(A.sNoFooter):0<B.length&&(r.nTFoot=B[0],wa(r.aoFooter,r.nTFoot));if(g.aaData)for(h=0;h<g.aaData.length;h++)ea(r,g.aaData[h]);else(r.bDeferLoading||"dom"==P(r))&&Ga(r,k(r.nTBody).children("tr"));r.aiDisplay=r.aiDisplayMaster.slice();r.bInitialised=!0;!1===
|
||||
n&&za(r)};g.bStateSave?(M.bStateSave=!0,Q(r,"aoDrawCallback",Qa,"state_save"),dc(r,g,e)):e()}});b=null;return this},L,w,J,rb={},gc=/[\r\n\u2028]/g,Ta=/<.*?>/g,tc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,uc=/(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\|\$|\^|\-)/g,qb=/['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,ca=function(a){return a&&!0!==a&&"-"!==a?!1:!0},hc=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},ic=function(a,b){rb[b]||
|
||||
(rb[b]=new RegExp(hb(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(rb[b],"."):a},sb=function(a,b,c){var d="string"===typeof a;if(ca(a))return!0;b&&d&&(a=ic(a,b));c&&d&&(a=a.replace(qb,""));return!isNaN(parseFloat(a))&&isFinite(a)},jc=function(a,b,c){return ca(a)?!0:ca(a)||"string"===typeof a?sb(a.replace(Ta,""),b,c)?!0:null:null},T=function(a,b,c){var d=[],f=0,e=a.length;if(c!==q)for(;f<e;f++)a[f]&&a[f][b]&&d.push(a[f][b][c]);else for(;f<e;f++)a[f]&&d.push(a[f][b]);return d},
|
||||
Ca=function(a,b,c,d){var f=[],e=0,g=b.length;if(d!==q)for(;e<g;e++)a[b[e]][c]&&f.push(a[b[e]][c][d]);else for(;e<g;e++)f.push(a[b[e]][c]);return f},qa=function(a,b){var c=[];if(b===q){b=0;var d=a}else d=b,b=a;for(a=b;a<d;a++)c.push(a);return c},kc=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},Ja=function(a){a:{if(!(2>a.length)){var b=a.slice().sort();for(var c=b[0],d=1,f=b.length;d<f;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];f=a.length;var e,
|
||||
g=0;d=0;a:for(;d<f;d++){c=a[d];for(e=0;e<g;e++)if(b[e]===c)continue a;b.push(c);g++}return b},lc=function(a,b){if(Array.isArray(b))for(var c=0;c<b.length;c++)lc(a,b[c]);else a.push(b);return a};Array.isArray||(Array.isArray=function(a){return"[object Array]"===Object.prototype.toString.call(a)});String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")});u.util={throttle:function(a,b){var c=b!==q?b:200,d,f;return function(){var e=this,g=
|
||||
+new Date,h=arguments;d&&g<d+c?(clearTimeout(f),f=setTimeout(function(){d=q;a.apply(e,h)},c)):(d=g,a.apply(e,h))}},escapeRegex:function(a){return a.replace(uc,"\\$1")}};var R=function(a,b,c){a[b]!==q&&(a[c]=a[b])},ua=/\[.*?\]$/,oa=/\(\)$/,hb=u.util.escapeRegex,Oa=k("<div>")[0],rc=Oa.textContent!==q,sc=/<.*?>/g,fb=u.util.throttle,mc=[],N=Array.prototype,vc=function(a){var b,c=u.settings,d=k.map(c,function(e,g){return e.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase()){var f=
|
||||
k.inArray(a,d);return-1!==f?[c[f]]:null}if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?b=k(a):a instanceof k&&(b=a)}else return[];if(b)return b.map(function(e){f=k.inArray(this,d);return-1!==f?c[f]:null}).toArray()};var D=function(a,b){if(!(this instanceof D))return new D(a,b);var c=[],d=function(g){(g=vc(g))&&c.push.apply(c,g)};if(Array.isArray(a))for(var f=0,e=a.length;f<e;f++)d(a[f]);else d(a);this.context=Ja(c);b&&k.merge(this,b);this.selector={rows:null,
|
||||
cols:null,opts:null};D.extend(this,this,mc)};u.Api=D;k.extend(D.prototype,{any:function(){return 0!==this.count()},concat:N.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new D(b[a],this[a]):null},filter:function(a){var b=[];if(N.filter)b=N.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);
|
||||
return new D(this.context,b)},flatten:function(){var a=[];return new D(this.context,a.concat.apply(a,this.toArray()))},join:N.join,indexOf:N.indexOf||function(a,b){b=b||0;for(var c=this.length;b<c;b++)if(this[b]===a)return b;return-1},iterator:function(a,b,c,d){var f=[],e,g,h=this.context,l,n=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);var m=0;for(e=h.length;m<e;m++){var p=new D(h[m]);if("table"===b){var t=c.call(p,h[m],m);t!==q&&f.push(t)}else if("columns"===b||"rows"===b)t=c.call(p,h[m],
|
||||
this[m],m),t!==q&&f.push(t);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){var v=this[m];"column-rows"===b&&(l=Ua(h[m],n.opts));var x=0;for(g=v.length;x<g;x++)t=v[x],t="cell"===b?c.call(p,h[m],t.row,t.column,m,x):c.call(p,h[m],t,m,x,l),t!==q&&f.push(t)}}return f.length||d?(a=new D(h,a?f.concat.apply([],f):f),b=a.selector,b.rows=n.rows,b.cols=n.cols,b.opts=n.opts,a):this},lastIndexOf:N.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,
|
||||
map:function(a){var b=[];if(N.map)b=N.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new D(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:N.pop,push:N.push,reduce:N.reduce||function(a,b){return Bb(this,a,b,0,this.length,1)},reduceRight:N.reduceRight||function(a,b){return Bb(this,a,b,this.length-1,-1,-1)},reverse:N.reverse,selector:null,shift:N.shift,slice:function(){return new D(this.context,this)},sort:N.sort,
|
||||
splice:N.splice,toArray:function(){return N.slice.call(this)},to$:function(){return k(this)},toJQuery:function(){return k(this)},unique:function(){return new D(this.context,Ja(this))},unshift:N.unshift});D.extend=function(a,b,c){if(c.length&&b&&(b instanceof D||b.__dt_wrapper)){var d,f=function(h,l,n){return function(){var m=l.apply(h,arguments);D.extend(m,m,n.methodExt);return m}};var e=0;for(d=c.length;e<d;e++){var g=c[e];b[g.name]="function"===g.type?f(a,g.val,g):"object"===g.type?{}:g.val;b[g.name].__dt_wrapper=
|
||||
!0;D.extend(a,b[g.name],g.propExt)}}};D.register=w=function(a,b){if(Array.isArray(a))for(var c=0,d=a.length;c<d;c++)D.register(a[c],b);else{d=a.split(".");var f=mc,e;a=0;for(c=d.length;a<c;a++){var g=(e=-1!==d[a].indexOf("()"))?d[a].replace("()",""):d[a];a:{var h=0;for(var l=f.length;h<l;h++)if(f[h].name===g){h=f[h];break a}h=null}h||(h={name:g,val:{},methodExt:[],propExt:[],type:"object"},f.push(h));a===c-1?(h.val=b,h.type="function"===typeof b?"function":k.isPlainObject(b)?"object":"other"):f=e?
|
||||
h.methodExt:h.propExt}}};D.registerPlural=J=function(a,b,c){D.register(a,c);D.register(b,function(){var d=c.apply(this,arguments);return d===this?this:d instanceof D?d.length?Array.isArray(d[0])?new D(d.context,d[0]):d[0]:q:d})};var nc=function(a,b){if(Array.isArray(a))return k.map(a,function(d){return nc(d,b)});if("number"===typeof a)return[b[a]];var c=k.map(b,function(d,f){return d.nTable});return k(c).filter(a).map(function(d){d=k.inArray(this,c);return b[d]}).toArray()};w("tables()",function(a){return a!==
|
||||
q&&null!==a?new D(nc(a,this.context)):this});w("table()",function(a){a=this.tables(a);var b=a.context;return b.length?new D(b[0]):a});J("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});J("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});J("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});J("tables().footer()","table().footer()",
|
||||
function(){return this.iterator("table",function(a){return a.nTFoot},1)});J("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});w("draw()",function(a){return this.iterator("table",function(b){"page"===a?fa(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),ja(b,!1===a))})});w("page()",function(a){return a===q?this.page.info().page:this.iterator("table",function(b){kb(b,a)})});w("page.info()",function(a){if(0===this.context.length)return q;
|
||||
a=this.context[0];var b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),f=-1===c;return{page:f?0:Math.floor(b/c),pages:f?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===P(a)}});w("page.len()",function(a){return a===q?0!==this.context.length?this.context[0]._iDisplayLength:q:this.iterator("table",function(b){ib(b,a)})});var oc=function(a,b,c){if(c){var d=new D(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==
|
||||
P(a))ja(a,b);else{U(a,!0);var f=a.jqXHR;f&&4!==f.readyState&&f.abort();La(a,[],function(e){Ha(a);e=Ma(a,e);for(var g=0,h=e.length;g<h;g++)ea(a,e[g]);ja(a,b);U(a,!1)})}};w("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});w("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});w("ajax.reload()",function(a,b){return this.iterator("table",function(c){oc(c,!1===b,a)})});w("ajax.url()",function(a){var b=this.context;if(a===q){if(0===b.length)return q;
|
||||
b=b[0];return b.ajax?k.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(c){k.isPlainObject(c.ajax)?c.ajax.url=a:c.ajax=a})});w("ajax.url().load()",function(a,b){return this.iterator("table",function(c){oc(c,!1===b,a)})});var tb=function(a,b,c,d,f){var e=[],g,h,l;var n=typeof b;b&&"string"!==n&&"function"!==n&&b.length!==q||(b=[b]);n=0;for(h=b.length;n<h;n++){var m=b[n]&&b[n].split&&!b[n].match(/[\[\(:]/)?b[n].split(","):[b[n]];var p=0;for(l=m.length;p<l;p++)(g=
|
||||
c("string"===typeof m[p]?m[p].trim():m[p]))&&g.length&&(e=e.concat(g))}a=L.selector[a];if(a.length)for(n=0,h=a.length;n<h;n++)e=a[n](d,f,e);return Ja(e)},ub=function(a){a||(a={});a.filter&&a.search===q&&(a.search=a.filter);return k.extend({search:"none",order:"current",page:"all"},a)},vb=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Ua=function(a,b){var c=[],d=a.aiDisplay;var f=a.aiDisplayMaster;
|
||||
var e=b.search;var g=b.order;b=b.page;if("ssp"==P(a))return"removed"===e?[]:qa(0,f.length);if("current"==b)for(g=a._iDisplayStart,a=a.fnDisplayEnd();g<a;g++)c.push(d[g]);else if("current"==g||"applied"==g)if("none"==e)c=f.slice();else if("applied"==e)c=d.slice();else{if("removed"==e){var h={};g=0;for(a=d.length;g<a;g++)h[d[g]]=null;c=k.map(f,function(l){return h.hasOwnProperty(l)?null:l})}}else if("index"==g||"original"==g)for(g=0,a=a.aoData.length;g<a;g++)"none"==e?c.push(g):(f=k.inArray(g,d),(-1===
|
||||
f&&"removed"==e||0<=f&&"applied"==e)&&c.push(g));return c},wc=function(a,b,c){var d;return tb("row",b,function(f){var e=hc(f),g=a.aoData;if(null!==e&&!c)return[e];d||(d=Ua(a,c));if(null!==e&&-1!==k.inArray(e,d))return[e];if(null===f||f===q||""===f)return d;if("function"===typeof f)return k.map(d,function(l){var n=g[l];return f(l,n._aData,n.nTr)?l:null});if(f.nodeName){e=f._DT_RowIndex;var h=f._DT_CellIndex;if(e!==q)return g[e]&&g[e].nTr===f?[e]:[];if(h)return g[h.row]&&g[h.row].nTr===f.parentNode?
|
||||
[h.row]:[];e=k(f).closest("*[data-dt-row]");return e.length?[e.data("dt-row")]:[]}if("string"===typeof f&&"#"===f.charAt(0)&&(e=a.aIds[f.replace(/^#/,"")],e!==q))return[e.idx];e=kc(Ca(a.aoData,d,"nTr"));return k(e).filter(f).map(function(){return this._DT_RowIndex}).toArray()},a,c)};w("rows()",function(a,b){a===q?a="":k.isPlainObject(a)&&(b=a,a="");b=ub(b);var c=this.iterator("table",function(d){return wc(d,a,b)},1);c.selector.rows=a;c.selector.opts=b;return c});w("rows().nodes()",function(){return this.iterator("row",
|
||||
function(a,b){return a.aoData[b].nTr||q},1)});w("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return Ca(a.aoData,b,"_aData")},1)});J("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){b=b.aoData[c];return"search"===a?b._aFilterData:b._aSortData},1)});J("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){va(b,c,a)})});J("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,
|
||||
b){return b},1)});J("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,f=c.length;d<f;d++)for(var e=0,g=this[d].length;e<g;e++){var h=c[d].rowIdFn(c[d].aoData[this[d][e]]._aData);b.push((!0===a?"#":"")+h)}return new D(c,b)});J("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var f=b.aoData,e=f[c],g,h;f.splice(c,1);var l=0;for(g=f.length;l<g;l++){var n=f[l];var m=n.anCells;null!==n.nTr&&(n.nTr._DT_RowIndex=l);if(null!==m)for(n=0,h=m.length;n<
|
||||
h;n++)m[n]._DT_CellIndex.row=l}Ia(b.aiDisplayMaster,c);Ia(b.aiDisplay,c);Ia(a[d],c,!1);0<b._iRecordsDisplay&&b._iRecordsDisplay--;jb(b);c=b.rowIdFn(e._aData);c!==q&&delete b.aIds[c]});this.iterator("table",function(b){for(var c=0,d=b.aoData.length;c<d;c++)b.aoData[c].idx=c});return this});w("rows.add()",function(a){var b=this.iterator("table",function(d){var f,e=[];var g=0;for(f=a.length;g<f;g++){var h=a[g];h.nodeName&&"TR"===h.nodeName.toUpperCase()?e.push(Ga(d,h)[0]):e.push(ea(d,h))}return e},1),
|
||||
c=this.rows(-1);c.pop();k.merge(c,b);return c});w("row()",function(a,b){return vb(this.rows(a,b))});w("row().data()",function(a){var b=this.context;if(a===q)return b.length&&this.length?b[0].aoData[this[0]]._aData:q;var c=b[0].aoData[this[0]];c._aData=a;Array.isArray(a)&&c.nTr&&c.nTr.id&&da(b[0].rowId)(a,c.nTr.id);va(b[0],this[0],"data");return this});w("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});w("row.add()",function(a){a instanceof
|
||||
k&&a.length&&(a=a[0]);var b=this.iterator("table",function(c){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?Ga(c,a)[0]:ea(c,a)});return this.row(b[0])});var xc=function(a,b,c,d){var f=[],e=function(g,h){if(Array.isArray(g)||g instanceof k)for(var l=0,n=g.length;l<n;l++)e(g[l],h);else g.nodeName&&"tr"===g.nodeName.toLowerCase()?f.push(g):(l=k("<tr><td></td></tr>").addClass(h),k("td",l).addClass(h).html(g)[0].colSpan=na(a),f.push(l[0]))};e(c,d);b._details&&b._details.detach();b._details=k(f);b._detailsShow&&
|
||||
b._details.insertAfter(b.nTr)},wb=function(a,b){var c=a.context;c.length&&(a=c[0].aoData[b!==q?b:a[0]])&&a._details&&(a._details.remove(),a._detailsShow=q,a._details=q)},pc=function(a,b){var c=a.context;c.length&&a.length&&(a=c[0].aoData[a[0]],a._details&&((a._detailsShow=b)?a._details.insertAfter(a.nTr):a._details.detach(),yc(c[0])))},yc=function(a){var b=new D(a),c=a.aoData;b.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<T(c,"_details").length&&(b.on("draw.dt.DT_details",
|
||||
function(d,f){a===f&&b.rows({page:"current"}).eq(0).each(function(e){e=c[e];e._detailsShow&&e._details.insertAfter(e.nTr)})}),b.on("column-visibility.dt.DT_details",function(d,f,e,g){if(a===f)for(f=na(f),e=0,g=c.length;e<g;e++)d=c[e],d._details&&d._details.children("td[colspan]").attr("colspan",f)}),b.on("destroy.dt.DT_details",function(d,f){if(a===f)for(d=0,f=c.length;d<f;d++)c[d]._details&&wb(b,d)}))};w("row().child()",function(a,b){var c=this.context;if(a===q)return c.length&&this.length?c[0].aoData[this[0]]._details:
|
||||
q;!0===a?this.child.show():!1===a?wb(this):c.length&&this.length&&xc(c[0],c[0].aoData[this[0]],a,b);return this});w(["row().child.show()","row().child().show()"],function(a){pc(this,!0);return this});w(["row().child.hide()","row().child().hide()"],function(){pc(this,!1);return this});w(["row().child.remove()","row().child().remove()"],function(){wb(this);return this});w("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var zc=
|
||||
/^([^:]+):(name|visIdx|visible)$/,qc=function(a,b,c,d,f){c=[];d=0;for(var e=f.length;d<e;d++)c.push(S(a,f[d],b));return c},Ac=function(a,b,c){var d=a.aoColumns,f=T(d,"sName"),e=T(d,"nTh");return tb("column",b,function(g){var h=hc(g);if(""===g)return qa(d.length);if(null!==h)return[0<=h?h:d.length+h];if("function"===typeof g){var l=Ua(a,c);return k.map(d,function(p,t){return g(t,qc(a,t,0,0,l),e[t])?t:null})}var n="string"===typeof g?g.match(zc):"";if(n)switch(n[2]){case "visIdx":case "visible":h=parseInt(n[1],
|
||||
10);if(0>h){var m=k.map(d,function(p,t){return p.bVisible?t:null});return[m[m.length+h]]}return[sa(a,h)];case "name":return k.map(f,function(p,t){return p===n[1]?t:null});default:return[]}if(g.nodeName&&g._DT_CellIndex)return[g._DT_CellIndex.column];h=k(e).filter(g).map(function(){return k.inArray(this,e)}).toArray();if(h.length||!g.nodeName)return h;h=k(g).closest("*[data-dt-column]");return h.length?[h.data("dt-column")]:[]},a,c)};w("columns()",function(a,b){a===q?a="":k.isPlainObject(a)&&(b=a,
|
||||
a="");b=ub(b);var c=this.iterator("table",function(d){return Ac(d,a,b)},1);c.selector.cols=a;c.selector.opts=b;return c});J("columns().header()","column().header()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTh},1)});J("columns().footer()","column().footer()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTf},1)});J("columns().data()","column().data()",function(){return this.iterator("column-rows",qc,1)});J("columns().dataSrc()",
|
||||
"column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});J("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,f,e){return Ca(b.aoData,e,"search"===a?"_aFilterData":"_aSortData",c)},1)});J("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,f){return Ca(a.aoData,f,"anCells",b)},1)});J("columns().visible()","column().visible()",function(a,b){var c=
|
||||
this,d=this.iterator("column",function(f,e){if(a===q)return f.aoColumns[e].bVisible;var g=f.aoColumns,h=g[e],l=f.aoData,n;if(a!==q&&h.bVisible!==a){if(a){var m=k.inArray(!0,T(g,"bVisible"),e+1);g=0;for(n=l.length;g<n;g++){var p=l[g].nTr;f=l[g].anCells;p&&p.insertBefore(f[e],f[m]||null)}}else k(T(f.aoData,"anCells",e)).detach();h.bVisible=a}});a!==q&&this.iterator("table",function(f){xa(f,f.aoHeader);xa(f,f.aoFooter);f.aiDisplay.length||k(f.nTBody).find("td[colspan]").attr("colspan",na(f));Qa(f);c.iterator("column",
|
||||
function(e,g){I(e,null,"column-visibility",[e,g,a,b])});(b===q||b)&&c.columns.adjust()});return d});J("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?ta(b,c):c},1)});w("columns.adjust()",function(){return this.iterator("table",function(a){ra(a)},1)});w("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return sa(c,b);if("fromData"===a||"toVisible"===a)return ta(c,b)}});
|
||||
w("column()",function(a,b){return vb(this.columns(a,b))});var Bc=function(a,b,c){var d=a.aoData,f=Ua(a,c),e=kc(Ca(d,f,"anCells")),g=k(lc([],e)),h,l=a.aoColumns.length,n,m,p,t,v,x;return tb("cell",b,function(r){var A="function"===typeof r;if(null===r||r===q||A){n=[];m=0;for(p=f.length;m<p;m++)for(h=f[m],t=0;t<l;t++)v={row:h,column:t},A?(x=d[h],r(v,S(a,h,t),x.anCells?x.anCells[t]:null)&&n.push(v)):n.push(v);return n}if(k.isPlainObject(r))return r.column!==q&&r.row!==q&&-1!==k.inArray(r.row,f)?[r]:[];
|
||||
A=g.filter(r).map(function(E,H){return{row:H._DT_CellIndex.row,column:H._DT_CellIndex.column}}).toArray();if(A.length||!r.nodeName)return A;x=k(r).closest("*[data-dt-row]");return x.length?[{row:x.data("dt-row"),column:x.data("dt-column")}]:[]},a,c)};w("cells()",function(a,b,c){k.isPlainObject(a)&&(a.row===q?(c=a,a=null):(c=b,b=null));k.isPlainObject(b)&&(c=b,b=null);if(null===b||b===q)return this.iterator("table",function(m){return Bc(m,a,ub(c))});var d=c?{page:c.page,order:c.order,search:c.search}:
|
||||
{},f=this.columns(b,d),e=this.rows(a,d),g,h,l,n;d=this.iterator("table",function(m,p){m=[];g=0;for(h=e[p].length;g<h;g++)for(l=0,n=f[p].length;l<n;l++)m.push({row:e[p][g],column:f[p][l]});return m},1);d=c&&c.selected?this.cells(d,c):d;k.extend(d.selector,{cols:b,rows:a,opts:c});return d});J("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&a.anCells?a.anCells[c]:q},1)});w("cells().data()",function(){return this.iterator("cell",function(a,
|
||||
b,c){return S(a,b,c)},1)});J("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});J("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return S(b,c,d,a)},1)});J("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,b,c){return{row:b,column:c,columnVisible:ta(a,c)}},1)});J("cells().invalidate()","cell().invalidate()",
|
||||
function(a){return this.iterator("cell",function(b,c,d){va(b,c,a,d)})});w("cell()",function(a,b,c){return vb(this.cells(a,b,c))});w("cell().data()",function(a){var b=this.context,c=this[0];if(a===q)return b.length&&c.length?S(b[0],c[0].row,c[0].column):q;Db(b[0],c[0].row,c[0].column,a);va(b[0],c[0].row,"data",c[0].column);return this});w("order()",function(a,b){var c=this.context;if(a===q)return 0!==c.length?c[0].aaSorting:q;"number"===typeof a?a=[[a,b]]:a.length&&!Array.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));
|
||||
return this.iterator("table",function(d){d.aaSorting=a.slice()})});w("order.listener()",function(a,b,c){return this.iterator("table",function(d){db(d,a,b,c)})});w("order.fixed()",function(a){if(!a){var b=this.context;b=b.length?b[0].aaSortingFixed:q;return Array.isArray(b)?{pre:b}:b}return this.iterator("table",function(c){c.aaSortingFixed=k.extend(!0,{},a)})});w(["columns().order()","column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var f=[];k.each(b[d],function(e,
|
||||
g){f.push([g,a])});c.aaSorting=f})});w("search()",function(a,b,c,d){var f=this.context;return a===q?0!==f.length?f[0].oPreviousSearch.sSearch:q:this.iterator("table",function(e){e.oFeatures.bFilter&&ya(e,k.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});J("columns().search()","column().search()",function(a,b,c,d){return this.iterator("column",function(f,e){var g=f.aoPreSearchCols;if(a===q)return g[e].sSearch;f.oFeatures.bFilter&&
|
||||
(k.extend(g[e],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),ya(f,f.oPreviousSearch,1))})});w("state()",function(){return this.context.length?this.context[0].oSavedState:null});w("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});w("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null});w("state.save()",function(){return this.iterator("table",function(a){Qa(a)})});
|
||||
u.versionCheck=u.fnVersionCheck=function(a){var b=u.version.split(".");a=a.split(".");for(var c,d,f=0,e=a.length;f<e;f++)if(c=parseInt(b[f],10)||0,d=parseInt(a[f],10)||0,c!==d)return c>d;return!0};u.isDataTable=u.fnIsDataTable=function(a){var b=k(a).get(0),c=!1;if(a instanceof u.Api)return!0;k.each(u.settings,function(d,f){d=f.nScrollHead?k("table",f.nScrollHead)[0]:null;var e=f.nScrollFoot?k("table",f.nScrollFoot)[0]:null;if(f.nTable===b||d===b||e===b)c=!0});return c};u.tables=u.fnTables=function(a){var b=
|
||||
!1;k.isPlainObject(a)&&(b=a.api,a=a.visible);var c=k.map(u.settings,function(d){if(!a||a&&k(d.nTable).is(":visible"))return d.nTable});return b?new D(c):c};u.camelToHungarian=O;w("$()",function(a,b){b=this.rows(b).nodes();b=k(b);return k([].concat(b.filter(a).toArray(),b.find(a).toArray()))});k.each(["on","one","off"],function(a,b){w(b+"()",function(){var c=Array.prototype.slice.call(arguments);c[0]=k.map(c[0].split(/\s/),function(f){return f.match(/\.dt\b/)?f:f+".dt"}).join(" ");var d=k(this.tables().nodes());
|
||||
d[b].apply(d,c);return this})});w("clear()",function(){return this.iterator("table",function(a){Ha(a)})});w("settings()",function(){return new D(this.context,this.context)});w("init()",function(){var a=this.context;return a.length?a[0].oInit:null});w("data()",function(){return this.iterator("table",function(a){return T(a.aoData,"_aData")}).flatten()});w("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,f=b.nTable,e=b.nTBody,g=b.nTHead,
|
||||
h=b.nTFoot,l=k(f);e=k(e);var n=k(b.nTableWrapper),m=k.map(b.aoData,function(t){return t.nTr}),p;b.bDestroying=!0;I(b,"aoDestroyCallback","destroy",[b]);a||(new D(b)).columns().visible(!0);n.off(".DT").find(":not(tbody *)").off(".DT");k(y).off(".DT-"+b.sInstance);f!=g.parentNode&&(l.children("thead").detach(),l.append(g));h&&f!=h.parentNode&&(l.children("tfoot").detach(),l.append(h));b.aaSorting=[];b.aaSortingFixed=[];Pa(b);k(m).removeClass(b.asStripeClasses.join(" "));k("th, td",g).removeClass(d.sSortable+
|
||||
" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);e.children().detach();e.append(m);g=a?"remove":"detach";l[g]();n[g]();!a&&c&&(c.insertBefore(f,b.nTableReinsertBefore),l.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&e.children().each(function(t){k(this).addClass(b.asDestroyStripes[t%p])}));c=k.inArray(b,u.settings);-1!==c&&u.settings.splice(c,1)})});k.each(["column","row","cell"],function(a,b){w(b+"s().every()",function(c){var d=this.selector.opts,f=
|
||||
this;return this.iterator(b,function(e,g,h,l,n){c.call(f[b](g,"cell"===b?h:d,"cell"===b?d:q),g,h,l,n)})})});w("i18n()",function(a,b,c){var d=this.context[0];a=ia(a)(d.oLanguage);a===q&&(a=b);c!==q&&k.isPlainObject(a)&&(a=a[c]!==q?a[c]:a._);return a.replace("%d",c)});u.version="1.10.22";u.settings=[];u.models={};u.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};u.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,
|
||||
idx:-1};u.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};u.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,
|
||||
25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,
|
||||
fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){return{}}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},
|
||||
fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",
|
||||
sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:k.extend({},u.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};G(u.defaults);u.defaults.column={aDataSort:null,
|
||||
iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};G(u.defaults.column);u.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,
|
||||
iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],
|
||||
aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:q,oAjaxData:q,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,
|
||||
iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==P(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==P(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,f=this.oFeatures,
|
||||
e=f.bPaginate;return f.bServerSide?!1===e||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!e||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};u.ext=L={buttons:{},classes:{},builder:"bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:u.fnVersionCheck,
|
||||
iApiIndex:0,oJUIClasses:{},sVersion:u.version};k.extend(L,{afnFiltering:L.search,aTypes:L.type.detect,ofnSearch:L.type.search,oSort:L.type.order,afnSortData:L.order,aoFeatures:L.feature,oApi:L.internal,oStdClasses:L.classes,oPagination:L.pager});k.extend(u.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",
|
||||
sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",
|
||||
sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var ec=u.ext.pager;k.extend(ec,{simple:function(a,b){return["previous","next"]},full:function(a,b){return["first","previous","next","last"]},numbers:function(a,b){return[Ba(a,b)]},simple_numbers:function(a,b){return["previous",Ba(a,b),"next"]},
|
||||
full_numbers:function(a,b){return["first","previous",Ba(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",Ba(a,b),"last"]},_numbers:Ba,numbers_length:7});k.extend(!0,u.ext.renderer,{pageButton:{_:function(a,b,c,d,f,e){var g=a.oClasses,h=a.oLanguage.oPaginate,l=a.oLanguage.oAria.paginate||{},n,m,p=0,t=function(x,r){var A,E=g.sPageButtonDisabled,H=function(B){kb(a,B.data.action,!0)};var W=0;for(A=r.length;W<A;W++){var M=r[W];if(Array.isArray(M)){var C=k("<"+(M.DT_el||"div")+"/>").appendTo(x);
|
||||
t(C,M)}else{n=null;m=M;C=a.iTabIndex;switch(M){case "ellipsis":x.append('<span class="ellipsis">…</span>');break;case "first":n=h.sFirst;0===f&&(C=-1,m+=" "+E);break;case "previous":n=h.sPrevious;0===f&&(C=-1,m+=" "+E);break;case "next":n=h.sNext;if(0===e||f===e-1)C=-1,m+=" "+E;break;case "last":n=h.sLast;if(0===e||f===e-1)C=-1,m+=" "+E;break;default:n=a.fnFormatNumber(M+1),m=f===M?g.sPageButtonActive:""}null!==n&&(C=k("<a>",{"class":g.sPageButton+" "+m,"aria-controls":a.sTableId,"aria-label":l[M],
|
||||
"data-dt-idx":p,tabindex:C,id:0===c&&"string"===typeof M?a.sTableId+"_"+M:null}).html(n).appendTo(x),ob(C,{action:M},H),p++)}}};try{var v=k(b).find(z.activeElement).data("dt-idx")}catch(x){}t(k(b).empty(),d);v!==q&&k(b).find("[data-dt-idx="+v+"]").trigger("focus")}}});k.extend(u.ext.type.detect,[function(a,b){b=b.oLanguage.sDecimal;return sb(a,b)?"num"+b:null},function(a,b){if(a&&!(a instanceof Date)&&!tc.test(a))return null;b=Date.parse(a);return null!==b&&!isNaN(b)||ca(a)?"date":null},function(a,
|
||||
b){b=b.oLanguage.sDecimal;return sb(a,b,!0)?"num-fmt"+b:null},function(a,b){b=b.oLanguage.sDecimal;return jc(a,b)?"html-num"+b:null},function(a,b){b=b.oLanguage.sDecimal;return jc(a,b,!0)?"html-num-fmt"+b:null},function(a,b){return ca(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);k.extend(u.ext.type.search,{html:function(a){return ca(a)?a:"string"===typeof a?a.replace(gc," ").replace(Ta,""):""},string:function(a){return ca(a)?a:"string"===typeof a?a.replace(gc," "):a}});var Sa=function(a,
|
||||
b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=ic(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};k.extend(L.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return ca(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return ca(a)?"":"string"===typeof a?a.toLowerCase():a.toString?a.toString():""},"string-asc":function(a,b){return a<b?-1:a>b?1:0},"string-desc":function(a,b){return a<
|
||||
b?1:a>b?-1:0}});Va("");k.extend(!0,u.ext.renderer,{header:{_:function(a,b,c,d){k(a.nTable).on("order.dt.DT",function(f,e,g,h){a===e&&(f=c.idx,b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass("asc"==h[f]?d.sSortAsc:"desc"==h[f]?d.sSortDesc:c.sSortingClass))})},jqueryui:function(a,b,c,d){k("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(k("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);k(a.nTable).on("order.dt.DT",function(f,e,g,h){a===e&&
|
||||
(f=c.idx,b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass("asc"==h[f]?d.sSortAsc:"desc"==h[f]?d.sSortDesc:c.sSortingClass),b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass("asc"==h[f]?d.sSortJUIAsc:"desc"==h[f]?d.sSortJUIDesc:c.sSortingClassJUI))})}}});var xb=function(a){return"string"===typeof a?a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):a};u.render=
|
||||
{number:function(a,b,c,d,f){return{display:function(e){if("number"!==typeof e&&"string"!==typeof e)return e;var g=0>e?"-":"",h=parseFloat(e);if(isNaN(h))return xb(e);h=h.toFixed(c);e=Math.abs(h);h=parseInt(e,10);e=c?b+(e-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+e+(f||"")}}},text:function(){return{display:xb,filter:xb}}};k.extend(u.ext.internal,{_fnExternApiFunc:fc,_fnBuildAjax:La,_fnAjaxUpdate:Fb,_fnAjaxParameters:Ob,_fnAjaxUpdateDraw:Pb,_fnAjaxDataSrc:Ma,
|
||||
_fnAddColumn:Wa,_fnColumnOptions:Da,_fnAdjustColumnSizing:ra,_fnVisibleToColumnIndex:sa,_fnColumnIndexToVisible:ta,_fnVisbleColumns:na,_fnGetColumns:Fa,_fnColumnTypes:Ya,_fnApplyColumnDefs:Cb,_fnHungarianMap:G,_fnCamelToHungarian:O,_fnLanguageCompat:ma,_fnBrowserDetect:Ab,_fnAddData:ea,_fnAddTr:Ga,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==q?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return k.inArray(c,a.aoData[b].anCells)},_fnGetCellData:S,_fnSetCellData:Db,_fnSplitObjNotation:ab,
|
||||
_fnGetObjectDataFn:ia,_fnSetObjectDataFn:da,_fnGetDataMaster:bb,_fnClearTable:Ha,_fnDeleteIndex:Ia,_fnInvalidate:va,_fnGetRowElements:$a,_fnCreateTr:Za,_fnBuildHead:Eb,_fnDrawHead:xa,_fnDraw:fa,_fnReDraw:ja,_fnAddOptionsHtml:Hb,_fnDetectHeader:wa,_fnGetUniqueThs:Ka,_fnFeatureHtmlFilter:Jb,_fnFilterComplete:ya,_fnFilterCustom:Sb,_fnFilterColumn:Rb,_fnFilter:Qb,_fnFilterCreateSearch:gb,_fnEscapeRegex:hb,_fnFilterData:Tb,_fnFeatureHtmlInfo:Mb,_fnUpdateInfo:Wb,_fnInfoMacros:Xb,_fnInitialise:za,_fnInitComplete:Na,
|
||||
_fnLengthChange:ib,_fnFeatureHtmlLength:Ib,_fnFeatureHtmlPaginate:Nb,_fnPageChange:kb,_fnFeatureHtmlProcessing:Kb,_fnProcessingDisplay:U,_fnFeatureHtmlTable:Lb,_fnScrollDraw:Ea,_fnApplyToChildren:Z,_fnCalculateColumnWidths:Xa,_fnThrottle:fb,_fnConvertToWidth:Zb,_fnGetWidestNode:$b,_fnGetMaxLenString:ac,_fnStringToCss:K,_fnSortFlatten:pa,_fnSort:Gb,_fnSortAria:cc,_fnSortListener:nb,_fnSortAttachListener:db,_fnSortingClasses:Pa,_fnSortData:bc,_fnSaveState:Qa,_fnLoadState:dc,_fnSettingsFromNode:Ra,_fnLog:aa,
|
||||
_fnMap:V,_fnBindAction:ob,_fnCallbackReg:Q,_fnCallbackFire:I,_fnLengthOverflow:jb,_fnRenderer:eb,_fnDataSource:P,_fnRowAttributes:cb,_fnExtend:pb,_fnCalculateEnd:function(){}});k.fn.dataTable=u;u.$=k;k.fn.dataTableSettings=u.settings;k.fn.dataTableExt=u.ext;k.fn.DataTable=function(a){return k(this).dataTable(a).api()};k.each(u,function(a,b){k.fn.DataTable[a]=b});return k.fn.dataTable});
|
||||
|
||||
|
||||
/*!
|
||||
DataTables Bootstrap 4 integration
|
||||
©2011-2017 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<e;d++){var f=a[d];if(b.call(c,f,d,a))return{i:d,v:f}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b){var c=$jscomp.propertyToPolyfillSymbol[b];if(null==c)return a[b];c=a[c];return void 0!==c?c:a[b]};
|
||||
$jscomp.polyfill=function(a,b,c,e){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,c,e):$jscomp.polyfillUnisolated(a,b,c,e))};$jscomp.polyfillUnisolated=function(a,b,c,e){c=$jscomp.global;a=a.split(".");for(e=0;e<a.length-1;e++){var d=a[e];if(!(d in c))return;c=c[d]}a=a[a.length-1];e=c[a];b=b(e);b!=e&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})};
|
||||
$jscomp.polyfillIsolated=function(a,b,c,e){var d=a.split(".");a=1===d.length;e=d[0];e=!a&&e in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var f=0;f<d.length-1;f++){var l=d[f];if(!(l in e))return;e=e[l]}d=d[d.length-1];c=$jscomp.IS_SYMBOL_NATIVE&&"es6"===c?e[d]:null;b=b(c);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,d,{configurable:!0,writable:!0,value:b}):b!==c&&($jscomp.propertyToPolyfillSymbol[d]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(d):$jscomp.POLYFILL_PREFIX+d,d=
|
||||
$jscomp.propertyToPolyfillSymbol[d],$jscomp.defineProperty(e,d,{configurable:!0,writable:!0,value:b})))};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(b,c){return $jscomp.findInternal(this,b,c).v}},"es6","es3");
|
||||
(function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,c){b||(b=window);c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$);return a(c,b,b.document)}:a(jQuery,window,document)})(function(a,b,c,e){var d=a.fn.dataTable;a.extend(!0,d.defaults,{dom:"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
renderer:"bootstrap"});a.extend(d.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});d.ext.renderer.pageButton.bootstrap=function(f,l,A,B,m,t){var u=new d.Api(f),C=f.oClasses,n=f.oLanguage.oPaginate,D=f.oLanguage.oAria.paginate||{},h,k,v=0,y=function(q,w){var x,E=function(p){p.preventDefault();
|
||||
a(p.currentTarget).hasClass("disabled")||u.page()==p.data.action||u.page(p.data.action).draw("page")};var r=0;for(x=w.length;r<x;r++){var g=w[r];if(Array.isArray(g))y(q,g);else{k=h="";switch(g){case "ellipsis":h="…";k="disabled";break;case "first":h=n.sFirst;k=g+(0<m?"":" disabled");break;case "previous":h=n.sPrevious;k=g+(0<m?"":" disabled");break;case "next":h=n.sNext;k=g+(m<t-1?"":" disabled");break;case "last":h=n.sLast;k=g+(m<t-1?"":" disabled");break;default:h=g+1,k=m===g?"active":""}if(h){var F=
|
||||
a("<li>",{"class":C.sPageButton+" "+k,id:0===A&&"string"===typeof g?f.sTableId+"_"+g:null}).append(a("<a>",{href:"#","aria-controls":f.sTableId,"aria-label":D[g],"data-dt-idx":v,tabindex:f.iTabIndex,"class":"page-link"}).html(h)).appendTo(q);f.oApi._fnBindAction(F,{action:g},E);v++}}}};try{var z=a(l).find(c.activeElement).data("dt-idx")}catch(q){}y(a(l).empty().html('<ul class="pagination"/>').children("ul"),B);z!==e&&a(l).find("[data-dt-idx="+z+"]").trigger("focus")};return d});
|
||||
|
||||
|
||||
/*!
|
||||
Copyright 2009-2020 SpryMedia Ltd.
|
||||
|
||||
This source file is free software, available under the following license:
|
||||
MIT license - http://datatables.net/license/mit
|
||||
|
||||
This source file is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||
|
||||
For details please refer to: http://www.datatables.net
|
||||
FixedHeader 3.1.7
|
||||
©2009-2020 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(c,d,f){c instanceof String&&(c=String(c));for(var h=c.length,g=0;g<h;g++){var m=c[g];if(d.call(f,m,g,c))return{i:g,v:m}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(c,d,f){c!=Array.prototype&&c!=Object.prototype&&(c[d]=f.value)};$jscomp.getGlobal=function(c){c=["object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global,c];for(var d=0;d<c.length;++d){var f=c[d];if(f&&f.Math==Math)return f}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.polyfill=function(c,d,f,h){if(d){f=$jscomp.global;c=c.split(".");for(h=0;h<c.length-1;h++){var g=c[h];g in f||(f[g]={});f=f[g]}c=c[c.length-1];h=f[c];d=d(h);d!=h&&null!=d&&$jscomp.defineProperty(f,c,{configurable:!0,writable:!0,value:d})}};$jscomp.polyfill("Array.prototype.find",function(c){return c?c:function(c,f){return $jscomp.findInternal(this,c,f).v}},"es6","es3");
|
||||
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(d){return c(d,window,document)}):"object"===typeof exports?module.exports=function(d,f){d||(d=window);f&&f.fn.dataTable||(f=require("datatables.net")(d,f).$);return c(f,d,d.document)}:c(jQuery,window,document)})(function(c,d,f,h){var g=c.fn.dataTable,m=0,l=function(a,b){if(!(this instanceof l))throw"FixedHeader must be initialised with the 'new' keyword.";!0===b&&(b={});a=new g.Api(a);this.c=c.extend(!0,
|
||||
{},l.defaults,b);this.s={dt:a,position:{theadTop:0,tbodyTop:0,tfootTop:0,tfootBottom:0,width:0,left:0,tfootHeight:0,theadHeight:0,windowHeight:c(d).height(),visible:!0},headerMode:null,footerMode:null,autoWidth:a.settings()[0].oFeatures.bAutoWidth,namespace:".dtfc"+m++,scrollLeft:{header:-1,footer:-1},enable:!0};this.dom={floatingHeader:null,thead:c(a.table().header()),tbody:c(a.table().body()),tfoot:c(a.table().footer()),header:{host:null,floating:null,placeholder:null},footer:{host:null,floating:null,
|
||||
placeholder:null}};this.dom.header.host=this.dom.thead.parent();this.dom.footer.host=this.dom.tfoot.parent();a=a.settings()[0];if(a._fixedHeader)throw"FixedHeader already initialised on table "+a.nTable.id;a._fixedHeader=this;this._constructor()};c.extend(l.prototype,{destroy:function(){this.s.dt.off(".dtfc");c(d).off(this.s.namespace);this.c.header&&this._modeChange("in-place","header",!0);this.c.footer&&this.dom.tfoot.length&&this._modeChange("in-place","footer",!0)},enable:function(a,b){this.s.enable=
|
||||
a;if(b||b===h)this._positions(),this._scroll(!0)},enabled:function(){return this.s.enable},headerOffset:function(a){a!==h&&(this.c.headerOffset=a,this.update());return this.c.headerOffset},footerOffset:function(a){a!==h&&(this.c.footerOffset=a,this.update());return this.c.footerOffset},update:function(){var a=this.s.dt.table().node();c(a).is(":visible")?this.enable(!0,!1):this.enable(!1,!1);this._positions();this._scroll(!0)},_constructor:function(){var a=this,b=this.s.dt;c(d).on("scroll"+this.s.namespace,
|
||||
function(){a._scroll()}).on("resize"+this.s.namespace,g.util.throttle(function(){a.s.position.windowHeight=c(d).height();a.update()},50));var k=c(".fh-fixedHeader");!this.c.headerOffset&&k.length&&(this.c.headerOffset=k.outerHeight());k=c(".fh-fixedFooter");!this.c.footerOffset&&k.length&&(this.c.footerOffset=k.outerHeight());b.on("column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc responsive-display.dt.dtfc",function(){a.update()});b.on("destroy.dtfc",function(){a.destroy()});
|
||||
this._positions();this._scroll()},_clone:function(a,b){var k=this.s.dt,e=this.dom[a],f="header"===a?this.dom.thead:this.dom.tfoot;!b&&e.floating?e.floating.removeClass("fixedHeader-floating fixedHeader-locked"):(e.floating&&(e.placeholder.remove(),this._unsize(a),e.floating.children().detach(),e.floating.remove()),e.floating=c(k.table().node().cloneNode(!1)).css("table-layout","fixed").attr("aria-hidden","true").removeAttr("id").append(f).appendTo("body"),e.placeholder=f.clone(!1),e.placeholder.find("*[id]").removeAttr("id"),
|
||||
e.host.prepend(e.placeholder),this._matchWidths(e.placeholder,e.floating))},_matchWidths:function(a,b){var k=function(b){return c(b,a).map(function(){return c(this).width()}).toArray()},e=function(a,e){c(a,b).each(function(a){c(this).css({width:e[a],minWidth:e[a]})})},f=k("th");k=k("td");e("th",f);e("td",k)},_unsize:function(a){var b=this.dom[a].floating;b&&("footer"===a||"header"===a&&!this.s.autoWidth)?c("th, td",b).css({width:"",minWidth:""}):b&&"header"===a&&c("th, td",b).css("min-width","")},
|
||||
_horizontal:function(a,b){var c=this.dom[a],e=this.s.position,f=this.s.scrollLeft;c.floating&&f[a]!==b&&(c.floating.css("left",e.left-b),f[a]=b)},_modeChange:function(a,b,k){var e=this.dom[b],d=this.s.position,g=function(a){e.floating.attr("style",function(b,c){return(c||"")+"width: "+a+"px !important;"})},h=this.dom["footer"===b?"tfoot":"thead"],l=c.contains(h[0],f.activeElement)?f.activeElement:null;l&&l.blur();"in-place"===a?(e.placeholder&&(e.placeholder.remove(),e.placeholder=null),this._unsize(b),
|
||||
"header"===b?e.host.prepend(h):e.host.append(h),e.floating&&(e.floating.remove(),e.floating=null)):"in"===a?(this._clone(b,k),e.floating.addClass("fixedHeader-floating").css("header"===b?"top":"bottom",this.c[b+"Offset"]).css("left",d.left+"px"),g(d.width),"footer"===b&&e.floating.css("top","")):"below"===a?(this._clone(b,k),e.floating.addClass("fixedHeader-locked").css("top",d.tfootTop-d.theadHeight).css("left",d.left+"px"),g(d.width)):"above"===a&&(this._clone(b,k),e.floating.addClass("fixedHeader-locked").css("top",
|
||||
d.tbodyTop).css("left",d.left+"px"),g(d.width));l&&l!==f.activeElement&&setTimeout(function(){l.focus()},10);this.s.scrollLeft.header=-1;this.s.scrollLeft.footer=-1;this.s[b+"Mode"]=a},_positions:function(){var a=this.s.dt.table(),b=this.s.position,f=this.dom;a=c(a.node());var e=a.children("thead"),d=a.children("tfoot");f=f.tbody;b.visible=a.is(":visible");b.width=a.outerWidth();b.left=a.offset().left;b.theadTop=e.offset().top;b.tbodyTop=f.offset().top;b.tbodyHeight=f.outerHeight();b.theadHeight=
|
||||
b.tbodyTop-b.theadTop;d.length?(b.tfootTop=d.offset().top,b.tfootBottom=b.tfootTop+d.outerHeight(),b.tfootHeight=b.tfootBottom-b.tfootTop):(b.tfootTop=b.tbodyTop+f.outerHeight(),b.tfootBottom=b.tfootTop,b.tfootHeight=b.tfootTop)},_scroll:function(a){var b=c(f).scrollTop(),d=c(f).scrollLeft(),e=this.s.position;if(this.c.header){var g=this.s.enable?!e.visible||b<=e.theadTop-this.c.headerOffset?"in-place":b<=e.tfootTop-e.theadHeight-this.c.headerOffset?"in":"below":"in-place";(a||g!==this.s.headerMode)&&
|
||||
this._modeChange(g,"header",a);this._horizontal("header",d)}this.c.footer&&this.dom.tfoot.length&&(b=this.s.enable?!e.visible||b+e.windowHeight>=e.tfootBottom+this.c.footerOffset?"in-place":e.windowHeight+b>e.tbodyTop+e.tfootHeight+this.c.footerOffset?"in":"above":"in-place",(a||b!==this.s.footerMode)&&this._modeChange(b,"footer",a),this._horizontal("footer",d))}});l.version="3.1.7";l.defaults={header:!0,footer:!1,headerOffset:0,footerOffset:0};c.fn.dataTable.FixedHeader=l;c.fn.DataTable.FixedHeader=
|
||||
l;c(f).on("init.dt.dtfh",function(a,b,d){"dt"===a.namespace&&(a=b.oInit.fixedHeader,d=g.defaults.fixedHeader,!a&&!d||b._fixedHeader||(d=c.extend({},d,a),!1!==a&&new l(b,d)))});g.Api.register("fixedHeader()",function(){});g.Api.register("fixedHeader.adjust()",function(){return this.iterator("table",function(a){(a=a._fixedHeader)&&a.update()})});g.Api.register("fixedHeader.enable()",function(a){return this.iterator("table",function(b){b=b._fixedHeader;a=a!==h?a:!0;b&&a!==b.enabled()&&b.enable(a)})});
|
||||
g.Api.register("fixedHeader.enabled()",function(){if(this.context.length){var a=this.content[0]._fixedHeader;if(a)return a.enabled()}return!1});g.Api.register("fixedHeader.disable()",function(){return this.iterator("table",function(a){(a=a._fixedHeader)&&a.enabled()&&a.enable(!1)})});c.each(["header","footer"],function(a,b){g.Api.register("fixedHeader."+b+"Offset()",function(a){var c=this.context;return a===h?c.length&&c[0]._fixedHeader?c[0]._fixedHeader[b+"Offset"]():h:this.iterator("table",function(c){if(c=
|
||||
c._fixedHeader)c[b+"Offset"](a)})})});return l});
|
||||
|
||||
|
||||
/*!
|
||||
Bootstrap 4 styling wrapper for FixedHeader
|
||||
©2018 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-fixedheader"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.FixedHeader||require("datatables.net-fixedheader")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){return c.fn.dataTable});
|
||||
|
||||
|
||||
/*!
|
||||
Copyright 2014-2020 SpryMedia Ltd.
|
||||
|
||||
This source file is free software, available under the following license:
|
||||
MIT license - http://datatables.net/license/mit
|
||||
|
||||
This source file is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||
|
||||
For details please refer to: http://www.datatables.net
|
||||
Responsive 2.2.6
|
||||
2014-2020 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(b,k,m){b instanceof String&&(b=String(b));for(var n=b.length,p=0;p<n;p++){var y=b[p];if(k.call(m,y,p,b))return{i:p,v:y}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,k,m){if(b==Array.prototype||b==Object.prototype)return b;b[k]=m.value;return b};$jscomp.getGlobal=function(b){b=["object"==typeof globalThis&&globalThis,b,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var k=0;k<b.length;++k){var m=b[k];if(m&&m.Math==Math)return m}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(b,k){var m=$jscomp.propertyToPolyfillSymbol[k];if(null==m)return b[k];m=b[m];return void 0!==m?m:b[k]};
|
||||
$jscomp.polyfill=function(b,k,m,n){k&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(b,k,m,n):$jscomp.polyfillUnisolated(b,k,m,n))};$jscomp.polyfillUnisolated=function(b,k,m,n){m=$jscomp.global;b=b.split(".");for(n=0;n<b.length-1;n++){var p=b[n];if(!(p in m))return;m=m[p]}b=b[b.length-1];n=m[b];k=k(n);k!=n&&null!=k&&$jscomp.defineProperty(m,b,{configurable:!0,writable:!0,value:k})};
|
||||
$jscomp.polyfillIsolated=function(b,k,m,n){var p=b.split(".");b=1===p.length;n=p[0];n=!b&&n in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<p.length-1;y++){var z=p[y];if(!(z in n))return;n=n[z]}p=p[p.length-1];m=$jscomp.IS_SYMBOL_NATIVE&&"es6"===m?n[p]:null;k=k(m);null!=k&&(b?$jscomp.defineProperty($jscomp.polyfills,p,{configurable:!0,writable:!0,value:k}):k!==m&&($jscomp.propertyToPolyfillSymbol[p]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(p):$jscomp.POLYFILL_PREFIX+p,p=
|
||||
$jscomp.propertyToPolyfillSymbol[p],$jscomp.defineProperty(n,p,{configurable:!0,writable:!0,value:k})))};$jscomp.polyfill("Array.prototype.find",function(b){return b?b:function(k,m){return $jscomp.findInternal(this,k,m).v}},"es6","es3");
|
||||
(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(k){return b(k,window,document)}):"object"===typeof exports?module.exports=function(k,m){k||(k=window);m&&m.fn.dataTable||(m=require("datatables.net")(k,m).$);return b(m,k,k.document)}:b(jQuery,window,document)})(function(b,k,m,n){function p(a,c,d){var f=c+"-"+d;if(A[f])return A[f];var g=[];a=a.cell(c,d).node().childNodes;c=0;for(d=a.length;c<d;c++)g.push(a[c]);return A[f]=g}function y(a,c,d){var f=c+"-"+
|
||||
d;if(A[f]){a=a.cell(c,d).node();d=A[f][0].parentNode.childNodes;c=[];for(var g=0,l=d.length;g<l;g++)c.push(d[g]);d=0;for(g=c.length;d<g;d++)a.appendChild(c[d]);A[f]=n}}var z=b.fn.dataTable,u=function(a,c){if(!z.versionCheck||!z.versionCheck("1.10.10"))throw"DataTables Responsive requires DataTables 1.10.10 or newer";this.s={dt:new z.Api(a),columns:[],current:[]};this.s.dt.settings()[0].responsive||(c&&"string"===typeof c.details?c.details={type:c.details}:c&&!1===c.details?c.details={type:!1}:c&&
|
||||
!0===c.details&&(c.details={type:"inline"}),this.c=b.extend(!0,{},u.defaults,z.defaults.responsive,c),a.responsive=this,this._constructor())};b.extend(u.prototype,{_constructor:function(){var a=this,c=this.s.dt,d=c.settings()[0],f=b(k).innerWidth();c.settings()[0]._responsive=this;b(k).on("resize.dtr orientationchange.dtr",z.util.throttle(function(){var g=b(k).innerWidth();g!==f&&(a._resize(),f=g)}));d.oApi._fnCallbackReg(d,"aoRowCreatedCallback",function(g,l,h){-1!==b.inArray(!1,a.s.current)&&b(">td, >th",
|
||||
g).each(function(e){e=c.column.index("toData",e);!1===a.s.current[e]&&b(this).css("display","none")})});c.on("destroy.dtr",function(){c.off(".dtr");b(c.table().body()).off(".dtr");b(k).off("resize.dtr orientationchange.dtr");c.cells(".dtr-control").nodes().to$().removeClass("dtr-control");b.each(a.s.current,function(g,l){!1===l&&a._setColumnVis(g,!0)})});this.c.breakpoints.sort(function(g,l){return g.width<l.width?1:g.width>l.width?-1:0});this._classLogic();this._resizeAuto();d=this.c.details;!1!==
|
||||
d.type&&(a._detailsInit(),c.on("column-visibility.dtr",function(){a._timer&&clearTimeout(a._timer);a._timer=setTimeout(function(){a._timer=null;a._classLogic();a._resizeAuto();a._resize(!0);a._redrawChildren()},100)}),c.on("draw.dtr",function(){a._redrawChildren()}),b(c.table().node()).addClass("dtr-"+d.type));c.on("column-reorder.dtr",function(g,l,h){a._classLogic();a._resizeAuto();a._resize(!0)});c.on("column-sizing.dtr",function(){a._resizeAuto();a._resize()});c.on("preXhr.dtr",function(){var g=
|
||||
[];c.rows().every(function(){this.child.isShown()&&g.push(this.id(!0))});c.one("draw.dtr",function(){a._resizeAuto();a._resize();c.rows(g).every(function(){a._detailsDisplay(this,!1)})})});c.on("draw.dtr",function(){a._controlClass()}).on("init.dtr",function(g,l,h){"dt"===g.namespace&&(a._resizeAuto(),a._resize(),b.inArray(!1,a.s.current)&&c.columns.adjust())});this._resize()},_columnsVisiblity:function(a){var c=this.s.dt,d=this.s.columns,f,g=d.map(function(t,v){return{columnIdx:v,priority:t.priority}}).sort(function(t,
|
||||
v){return t.priority!==v.priority?t.priority-v.priority:t.columnIdx-v.columnIdx}),l=b.map(d,function(t,v){return!1===c.column(v).visible()?"not-visible":t.auto&&null===t.minWidth?!1:!0===t.auto?"-":-1!==b.inArray(a,t.includeIn)}),h=0;var e=0;for(f=l.length;e<f;e++)!0===l[e]&&(h+=d[e].minWidth);e=c.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;h=c.table().container().offsetWidth-e-h;e=0;for(f=l.length;e<f;e++)d[e].control&&(h-=d[e].minWidth);var r=!1;e=0;for(f=g.length;e<f;e++){var q=g[e].columnIdx;
|
||||
"-"===l[q]&&!d[q].control&&d[q].minWidth&&(r||0>h-d[q].minWidth?(r=!0,l[q]=!1):l[q]=!0,h-=d[q].minWidth)}g=!1;e=0;for(f=d.length;e<f;e++)if(!d[e].control&&!d[e].never&&!1===l[e]){g=!0;break}e=0;for(f=d.length;e<f;e++)d[e].control&&(l[e]=g),"not-visible"===l[e]&&(l[e]=!1);-1===b.inArray(!0,l)&&(l[0]=!0);return l},_classLogic:function(){var a=this,c=this.c.breakpoints,d=this.s.dt,f=d.columns().eq(0).map(function(h){var e=this.column(h),r=e.header().className;h=d.settings()[0].aoColumns[h].responsivePriority;
|
||||
e=e.header().getAttribute("data-priority");h===n&&(h=e===n||null===e?1E4:1*e);return{className:r,includeIn:[],auto:!1,control:!1,never:r.match(/\bnever\b/)?!0:!1,priority:h}}),g=function(h,e){h=f[h].includeIn;-1===b.inArray(e,h)&&h.push(e)},l=function(h,e,r,q){if(!r)f[h].includeIn.push(e);else if("max-"===r)for(q=a._find(e).width,e=0,r=c.length;e<r;e++)c[e].width<=q&&g(h,c[e].name);else if("min-"===r)for(q=a._find(e).width,e=0,r=c.length;e<r;e++)c[e].width>=q&&g(h,c[e].name);else if("not-"===r)for(e=
|
||||
0,r=c.length;e<r;e++)-1===c[e].name.indexOf(q)&&g(h,c[e].name)};f.each(function(h,e){for(var r=h.className.split(" "),q=!1,t=0,v=r.length;t<v;t++){var B=r[t].trim();if("all"===B){q=!0;h.includeIn=b.map(c,function(w){return w.name});return}if("none"===B||h.never){q=!0;return}if("control"===B||"dtr-control"===B){q=!0;h.control=!0;return}b.each(c,function(w,D){w=D.name.split("-");var x=B.match(new RegExp("(min\\-|max\\-|not\\-)?("+w[0]+")(\\-[_a-zA-Z0-9])?"));x&&(q=!0,x[2]===w[0]&&x[3]==="-"+w[1]?l(e,
|
||||
D.name,x[1],x[2]+x[3]):x[2]!==w[0]||x[3]||l(e,D.name,x[1],x[2]))})}q||(h.auto=!0)});this.s.columns=f},_controlClass:function(){if("inline"===this.c.details.type){var a=this.s.dt,c=b.inArray(!0,this.s.current);a.cells(null,function(d){return d!==c},{page:"current"}).nodes().to$().filter(".dtr-control").removeClass("dtr-control");a.cells(null,c,{page:"current"}).nodes().to$().addClass("dtr-control")}},_detailsDisplay:function(a,c){var d=this,f=this.s.dt,g=this.c.details;if(g&&!1!==g.type){var l=g.display(a,
|
||||
c,function(){return g.renderer(f,a[0],d._detailsObj(a[0]))});!0!==l&&!1!==l||b(f.table().node()).triggerHandler("responsive-display.dt",[f,a,l,c])}},_detailsInit:function(){var a=this,c=this.s.dt,d=this.c.details;"inline"===d.type&&(d.target="td.dtr-control, th.dtr-control");c.on("draw.dtr",function(){a._tabIndexes()});a._tabIndexes();b(c.table().body()).on("keyup.dtr","td, th",function(g){13===g.keyCode&&b(this).data("dtr-keyboard")&&b(this).click()});var f=d.target;d="string"===typeof f?f:"td, th";
|
||||
if(f!==n||null!==f)b(c.table().body()).on("click.dtr mousedown.dtr mouseup.dtr",d,function(g){if(b(c.table().node()).hasClass("collapsed")&&-1!==b.inArray(b(this).closest("tr").get(0),c.rows().nodes().toArray())){if("number"===typeof f){var l=0>f?c.columns().eq(0).length+f:f;if(c.cell(this).index().column!==l)return}l=c.row(b(this).closest("tr"));"click"===g.type?a._detailsDisplay(l,!1):"mousedown"===g.type?b(this).css("outline","none"):"mouseup"===g.type&&b(this).trigger("blur").css("outline","")}})},
|
||||
_detailsObj:function(a){var c=this,d=this.s.dt;return b.map(this.s.columns,function(f,g){if(!f.never&&!f.control)return f=d.settings()[0].aoColumns[g],{className:f.sClass,columnIndex:g,data:d.cell(a,g).render(c.c.orthogonal),hidden:d.column(g).visible()&&!c.s.current[g],rowIndex:a,title:null!==f.sTitle?f.sTitle:b(d.column(g).header()).text()}})},_find:function(a){for(var c=this.c.breakpoints,d=0,f=c.length;d<f;d++)if(c[d].name===a)return c[d]},_redrawChildren:function(){var a=this,c=this.s.dt;c.rows({page:"current"}).iterator("row",
|
||||
function(d,f){c.row(f);a._detailsDisplay(c.row(f),!0)})},_resize:function(a){var c=this,d=this.s.dt,f=b(k).innerWidth(),g=this.c.breakpoints,l=g[0].name,h=this.s.columns,e,r=this.s.current.slice();for(e=g.length-1;0<=e;e--)if(f<=g[e].width){l=g[e].name;break}var q=this._columnsVisiblity(l);this.s.current=q;g=!1;e=0;for(f=h.length;e<f;e++)if(!1===q[e]&&!h[e].never&&!h[e].control&&!1===!d.column(e).visible()){g=!0;break}b(d.table().node()).toggleClass("collapsed",g);var t=!1,v=0;d.columns().eq(0).each(function(B,
|
||||
w){!0===q[w]&&v++;if(a||q[w]!==r[w])t=!0,c._setColumnVis(B,q[w])});t&&(this._redrawChildren(),b(d.table().node()).trigger("responsive-resize.dt",[d,this.s.current]),0===d.page.info().recordsDisplay&&b("td",d.table().body()).eq(0).attr("colspan",v));c._controlClass()},_resizeAuto:function(){var a=this.s.dt,c=this.s.columns;if(this.c.auto&&-1!==b.inArray(!0,b.map(c,function(e){return e.auto}))){b.isEmptyObject(A)||b.each(A,function(e){e=e.split("-");y(a,1*e[0],1*e[1])});a.table().node();var d=a.table().node().cloneNode(!1),
|
||||
f=b(a.table().header().cloneNode(!1)).appendTo(d),g=b(a.table().body()).clone(!1,!1).empty().appendTo(d);d.style.width="auto";var l=a.columns().header().filter(function(e){return a.column(e).visible()}).to$().clone(!1).css("display","table-cell").css("width","auto").css("min-width",0);b(g).append(b(a.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(g=a.table().footer()){g=b(g.cloneNode(!1)).appendTo(d);var h=a.columns().footer().filter(function(e){return a.column(e).visible()}).to$().clone(!1).css("display",
|
||||
"table-cell");b("<tr/>").append(h).appendTo(g)}b("<tr/>").append(l).appendTo(f);"inline"===this.c.details.type&&b(d).addClass("dtr-inline collapsed");b(d).find("[name]").removeAttr("name");b(d).css("position","relative");d=b("<div/>").css({width:1,height:1,overflow:"hidden",clear:"both"}).append(d);d.insertBefore(a.table().node());l.each(function(e){e=a.column.index("fromVisible",e);c[e].minWidth=this.offsetWidth||0});d.remove()}},_responsiveOnlyHidden:function(){var a=this.s.dt;return b.map(this.s.current,
|
||||
function(c,d){return!1===a.column(d).visible()?!0:c})},_setColumnVis:function(a,c){var d=this.s.dt;c=c?"":"none";b(d.column(a).header()).css("display",c);b(d.column(a).footer()).css("display",c);d.column(a).nodes().to$().css("display",c);b.isEmptyObject(A)||d.cells(null,a).indexes().each(function(f){y(d,f.row,f.column)})},_tabIndexes:function(){var a=this.s.dt,c=a.cells({page:"current"}).nodes().to$(),d=a.settings()[0],f=this.c.details.target;c.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");
|
||||
"number"===typeof f?a.cells(null,f,{page:"current"}).nodes().to$().attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1):("td:first-child, th:first-child"===f&&(f=">td:first-child, >th:first-child"),b(f,a.rows({page:"current"}).nodes()).attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1))}});u.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];u.display={childRow:function(a,c,d){if(c){if(b(a.node()).hasClass("parent"))return a.child(d(),
|
||||
"child").show(),!0}else{if(a.child.isShown())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0}},childRowImmediate:function(a,c,d){if(!c&&a.child.isShown()||!a.responsive.hasHidden())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0},modal:function(a){return function(c,d,f){if(d)b("div.dtr-modal-content").empty().append(f());else{var g=function(){l.remove();
|
||||
b(m).off("keypress.dtr")},l=b('<div class="dtr-modal"/>').append(b('<div class="dtr-modal-display"/>').append(b('<div class="dtr-modal-content"/>').append(f())).append(b('<div class="dtr-modal-close">×</div>').click(function(){g()}))).append(b('<div class="dtr-modal-background"/>').click(function(){g()})).appendTo("body");b(m).on("keyup.dtr",function(h){27===h.keyCode&&(h.stopPropagation(),g())})}a&&a.header&&b("div.dtr-modal-content").prepend("<h2>"+a.header(c)+"</h2>")}}};var A={};u.renderer=
|
||||
{listHiddenNodes:function(){return function(a,c,d){var f=b('<ul data-dtr-index="'+c+'" class="dtr-details"/>'),g=!1;b.each(d,function(l,h){h.hidden&&(b("<li "+(h.className?'class="'+h.className+'"':"")+' data-dtr-index="'+h.columnIndex+'" data-dt-row="'+h.rowIndex+'" data-dt-column="'+h.columnIndex+'"><span class="dtr-title">'+h.title+"</span> </li>").append(b('<span class="dtr-data"/>').append(p(a,h.rowIndex,h.columnIndex))).appendTo(f),g=!0)});return g?f:!1}},listHidden:function(){return function(a,
|
||||
c,d){return(a=b.map(d,function(f){var g=f.className?'class="'+f.className+'"':"";return f.hidden?"<li "+g+' data-dtr-index="'+f.columnIndex+'" data-dt-row="'+f.rowIndex+'" data-dt-column="'+f.columnIndex+'"><span class="dtr-title">'+f.title+'</span> <span class="dtr-data">'+f.data+"</span></li>":""}).join(""))?b('<ul data-dtr-index="'+c+'" class="dtr-details"/>').append(a):!1}},tableAll:function(a){a=b.extend({tableClass:""},a);return function(c,d,f){c=b.map(f,function(g){return"<tr "+(g.className?
|
||||
'class="'+g.className+'"':"")+' data-dt-row="'+g.rowIndex+'" data-dt-column="'+g.columnIndex+'"><td>'+g.title+":</td> <td>"+g.data+"</td></tr>"}).join("");return b('<table class="'+a.tableClass+' dtr-details" width="100%"/>').append(c)}}};u.defaults={breakpoints:u.breakpoints,auto:!0,details:{display:u.display.childRow,renderer:u.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var C=b.fn.dataTable.Api;C.register("responsive()",function(){return this});C.register("responsive.index()",
|
||||
function(a){a=b(a);return{column:a.data("dtr-index"),row:a.parent().data("dtr-index")}});C.register("responsive.rebuild()",function(){return this.iterator("table",function(a){a._responsive&&a._responsive._classLogic()})});C.register("responsive.recalc()",function(){return this.iterator("table",function(a){a._responsive&&(a._responsive._resizeAuto(),a._responsive._resize())})});C.register("responsive.hasHidden()",function(){var a=this.context[0];return a._responsive?-1!==b.inArray(!1,a._responsive._responsiveOnlyHidden()):
|
||||
!1});C.registerPlural("columns().responsiveHidden()","column().responsiveHidden()",function(){return this.iterator("column",function(a,c){return a._responsive?a._responsive._responsiveOnlyHidden()[c]:!1},1)});u.version="2.2.6";b.fn.dataTable.Responsive=u;b.fn.DataTable.Responsive=u;b(m).on("preInit.dt.dtr",function(a,c,d){"dt"===a.namespace&&(b(c.nTable).hasClass("responsive")||b(c.nTable).hasClass("dt-responsive")||c.oInit.responsive||z.defaults.responsive)&&(a=c.oInit.responsive,!1!==a&&new u(c,
|
||||
b.isPlainObject(a)?a:{}))});return u});
|
||||
|
||||
|
||||
/*!
|
||||
Bootstrap 4 integration for DataTables' Responsive
|
||||
©2016 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<e;d++){var f=a[d];if(b.call(c,f,d,a))return{i:d,v:f}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b){var c=$jscomp.propertyToPolyfillSymbol[b];if(null==c)return a[b];c=a[c];return void 0!==c?c:a[b]};
|
||||
$jscomp.polyfill=function(a,b,c,e){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,c,e):$jscomp.polyfillUnisolated(a,b,c,e))};$jscomp.polyfillUnisolated=function(a,b,c,e){c=$jscomp.global;a=a.split(".");for(e=0;e<a.length-1;e++){var d=a[e];if(!(d in c))return;c=c[d]}a=a[a.length-1];e=c[a];b=b(e);b!=e&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})};
|
||||
$jscomp.polyfillIsolated=function(a,b,c,e){var d=a.split(".");a=1===d.length;e=d[0];e=!a&&e in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var f=0;f<d.length-1;f++){var g=d[f];if(!(g in e))return;e=e[g]}d=d[d.length-1];c=$jscomp.IS_SYMBOL_NATIVE&&"es6"===c?e[d]:null;b=b(c);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,d,{configurable:!0,writable:!0,value:b}):b!==c&&($jscomp.propertyToPolyfillSymbol[d]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(d):$jscomp.POLYFILL_PREFIX+d,d=
|
||||
$jscomp.propertyToPolyfillSymbol[d],$jscomp.defineProperty(e,d,{configurable:!0,writable:!0,value:b})))};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(b,c){return $jscomp.findInternal(this,b,c).v}},"es6","es3");
|
||||
(function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-responsive"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,c){b||(b=window);c&&c.fn.dataTable||(c=require("datatables.net-bs4")(b,c).$);c.fn.dataTable.Responsive||require("datatables.net-responsive")(b,c);return a(c,b,b.document)}:a(jQuery,window,document)})(function(a,b,c,e){b=a.fn.dataTable;c=b.Responsive.display;var d=c.modal,f=a('<div class="modal fade dtr-bs-modal" role="dialog"><div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button></div><div class="modal-body"/></div></div></div>');
|
||||
c.modal=function(g){return function(k,h,l){if(!a.fn.modal)d(k,h,l);else if(!h){if(g&&g.header){h=f.find("div.modal-header");var m=h.find("button").detach();h.empty().append('<h4 class="modal-title">'+g.header(k)+"</h4>").append(m)}f.find("div.modal-body").empty().append(l());f.appendTo("body").modal()}}};return b.Responsive});
|
||||
|
||||
|
||||
/*!
|
||||
Copyright 2011-2020 SpryMedia Ltd.
|
||||
|
||||
This source file is free software, available under the following license:
|
||||
MIT license - http://datatables.net/license/mit
|
||||
|
||||
This source file is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||
|
||||
For details please refer to: http://www.datatables.net
|
||||
Scroller 2.0.3
|
||||
©2011-2020 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(d,f,g){d instanceof String&&(d=String(d));for(var h=d.length,k=0;k<h;k++){var m=d[k];if(f.call(g,m,k,d))return{i:k,v:m}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(d,f,g){if(d==Array.prototype||d==Object.prototype)return d;d[f]=g.value;return d};$jscomp.getGlobal=function(d){d=["object"==typeof globalThis&&globalThis,d,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var f=0;f<d.length;++f){var g=d[f];if(g&&g.Math==Math)return g}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(d,f){var g=$jscomp.propertyToPolyfillSymbol[f];if(null==g)return d[f];g=d[g];return void 0!==g?g:d[f]};
|
||||
$jscomp.polyfill=function(d,f,g,h){f&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(d,f,g,h):$jscomp.polyfillUnisolated(d,f,g,h))};$jscomp.polyfillUnisolated=function(d,f,g,h){g=$jscomp.global;d=d.split(".");for(h=0;h<d.length-1;h++){var k=d[h];if(!(k in g))return;g=g[k]}d=d[d.length-1];h=g[d];f=f(h);f!=h&&null!=f&&$jscomp.defineProperty(g,d,{configurable:!0,writable:!0,value:f})};
|
||||
$jscomp.polyfillIsolated=function(d,f,g,h){var k=d.split(".");d=1===k.length;h=k[0];h=!d&&h in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var m=0;m<k.length-1;m++){var q=k[m];if(!(q in h))return;h=h[q]}k=k[k.length-1];g=$jscomp.IS_SYMBOL_NATIVE&&"es6"===g?h[k]:null;f=f(g);null!=f&&(d?$jscomp.defineProperty($jscomp.polyfills,k,{configurable:!0,writable:!0,value:f}):f!==g&&($jscomp.propertyToPolyfillSymbol[k]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(k):$jscomp.POLYFILL_PREFIX+k,k=
|
||||
$jscomp.propertyToPolyfillSymbol[k],$jscomp.defineProperty(h,k,{configurable:!0,writable:!0,value:f})))};$jscomp.polyfill("Array.prototype.find",function(d){return d?d:function(f,g){return $jscomp.findInternal(this,f,g).v}},"es6","es3");
|
||||
(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(f){return d(f,window,document)}):"object"===typeof exports?module.exports=function(f,g){f||(f=window);g&&g.fn.dataTable||(g=require("datatables.net")(f,g).$);return d(g,f,f.document)}:d(jQuery,window,document)})(function(d,f,g,h){var k=d.fn.dataTable,m=function(a,b){this instanceof m?(b===h&&(b={}),a=d.fn.dataTable.Api(a),this.s={dt:a.settings()[0],dtApi:a,tableTop:0,tableBottom:0,redrawTop:0,redrawBottom:0,
|
||||
autoHeight:!0,viewportRows:0,stateTO:null,stateSaveThrottle:function(){},drawTO:null,heights:{jump:null,page:null,virtual:null,scroll:null,row:null,viewport:null,labelFactor:1},topRowFloat:0,scrollDrawDiff:null,loaderVisible:!1,forceReposition:!1,baseRowTop:0,baseScrollTop:0,mousedown:!1,lastScrollTop:0},this.s=d.extend(this.s,m.oDefaults,b),this.s.heights.row=this.s.rowHeight,this.dom={force:g.createElement("div"),label:d('<div class="dts_label">0</div>'),scroller:null,table:null,loader:null},this.s.dt.oScroller||
|
||||
(this.s.dt.oScroller=this,this.construct())):alert("Scroller warning: Scroller must be initialised with the 'new' keyword.")};d.extend(m.prototype,{measure:function(a){this.s.autoHeight&&this._calcRowHeight();var b=this.s.heights;b.row&&(b.viewport=this._parseHeight(d(this.dom.scroller).css("max-height")),this.s.viewportRows=parseInt(b.viewport/b.row,10)+1,this.s.dt._iDisplayLength=this.s.viewportRows*this.s.displayBuffer);var c=this.dom.label.outerHeight();b.labelFactor=(b.viewport-c)/b.scroll;(a===
|
||||
h||a)&&this.s.dt.oInstance.fnDraw(!1)},pageInfo:function(){var a=this.dom.scroller.scrollTop,b=this.s.dt.fnRecordsDisplay(),c=Math.ceil(this.pixelsToRow(a+this.s.heights.viewport,!1,this.s.ani));return{start:Math.floor(this.pixelsToRow(a,!1,this.s.ani)),end:b<c?b-1:c-1}},pixelsToRow:function(a,b,c){a-=this.s.baseScrollTop;c=c?(this._domain("physicalToVirtual",this.s.baseScrollTop)+a)/this.s.heights.row:a/this.s.heights.row+this.s.baseRowTop;return b||b===h?parseInt(c,10):c},rowToPixels:function(a,
|
||||
b,c){a-=this.s.baseRowTop;c=c?this._domain("virtualToPhysical",this.s.baseScrollTop):this.s.baseScrollTop;c+=a*this.s.heights.row;return b||b===h?parseInt(c,10):c},scrollToRow:function(a,b){var c=this,e=!1,l=this.rowToPixels(a),n=a-(this.s.displayBuffer-1)/2*this.s.viewportRows;0>n&&(n=0);(l>this.s.redrawBottom||l<this.s.redrawTop)&&this.s.dt._iDisplayStart!==n&&(e=!0,l=this._domain("virtualToPhysical",a*this.s.heights.row),this.s.redrawTop<l&&l<this.s.redrawBottom&&(this.s.forceReposition=!0,b=!1));
|
||||
b===h||b?(this.s.ani=e,d(this.dom.scroller).animate({scrollTop:l},function(){setTimeout(function(){c.s.ani=!1},250)})):d(this.dom.scroller).scrollTop(l)},construct:function(){var a=this,b=this.s.dtApi;if(this.s.dt.oFeatures.bPaginate){this.dom.force.style.position="relative";this.dom.force.style.top="0px";this.dom.force.style.left="0px";this.dom.force.style.width="1px";this.dom.scroller=d("div."+this.s.dt.oClasses.sScrollBody,this.s.dt.nTableWrapper)[0];this.dom.scroller.appendChild(this.dom.force);
|
||||
this.dom.scroller.style.position="relative";this.dom.table=d(">table",this.dom.scroller)[0];this.dom.table.style.position="absolute";this.dom.table.style.top="0px";this.dom.table.style.left="0px";d(b.table().container()).addClass("dts DTS");this.s.loadingIndicator&&(this.dom.loader=d('<div class="dataTables_processing dts_loading">'+this.s.dt.oLanguage.sLoadingRecords+"</div>").css("display","none"),d(this.dom.scroller.parentNode).css("position","relative").append(this.dom.loader));this.dom.label.appendTo(this.dom.scroller);
|
||||
this.s.heights.row&&"auto"!=this.s.heights.row&&(this.s.autoHeight=!1);this.s.ingnoreScroll=!0;d(this.dom.scroller).on("scroll.dt-scroller",function(l){a._scroll.call(a)});d(this.dom.scroller).on("touchstart.dt-scroller",function(){a._scroll.call(a)});d(this.dom.scroller).on("mousedown.dt-scroller",function(){a.s.mousedown=!0}).on("mouseup.dt-scroller",function(){a.s.labelVisible=!1;a.s.mousedown=!1;a.dom.label.css("display","none")});d(f).on("resize.dt-scroller",function(){a.measure(!1);a._info()});
|
||||
var c=!0,e=b.state.loaded();b.on("stateSaveParams.scroller",function(l,n,p){c&&e?(p.scroller=e.scroller,c=!1):p.scroller={topRow:a.s.topRowFloat,baseScrollTop:a.s.baseScrollTop,baseRowTop:a.s.baseRowTop,scrollTop:a.s.lastScrollTop}});e&&e.scroller&&(this.s.topRowFloat=e.scroller.topRow,this.s.baseScrollTop=e.scroller.baseScrollTop,this.s.baseRowTop=e.scroller.baseRowTop);this.measure(!1);a.s.stateSaveThrottle=a.s.dt.oApi._fnThrottle(function(){a.s.dtApi.state.save()},500);b.on("init.scroller",function(){a.measure(!1);
|
||||
a.s.scrollType="jump";a._draw();b.on("draw.scroller",function(){a._draw()})});b.on("preDraw.dt.scroller",function(){a._scrollForce()});b.on("destroy.scroller",function(){d(f).off("resize.dt-scroller");d(a.dom.scroller).off(".dt-scroller");d(a.s.dt.nTable).off(".scroller");d(a.s.dt.nTableWrapper).removeClass("DTS");d("div.DTS_Loading",a.dom.scroller.parentNode).remove();a.dom.table.style.position="";a.dom.table.style.top="";a.dom.table.style.left=""})}else this.s.dt.oApi._fnLog(this.s.dt,0,"Pagination must be enabled for Scroller")},
|
||||
_calcRowHeight:function(){var a=this.s.dt,b=a.nTable,c=b.cloneNode(!1),e=d("<tbody/>").appendTo(c),l=d('<div class="'+a.oClasses.sWrapper+' DTS"><div class="'+a.oClasses.sScrollWrapper+'"><div class="'+a.oClasses.sScrollBody+'"></div></div></div>');d("tbody tr:lt(4)",b).clone().appendTo(e);var n=d("tr",e).length;if(1===n)e.prepend("<tr><td> </td></tr>"),e.append("<tr><td> </td></tr>");else for(;3>n;n++)e.append("<tr><td> </td></tr>");d("div."+a.oClasses.sScrollBody,l).append(c);a=this.s.dt.nHolding||
|
||||
b.parentNode;d(a).is(":visible")||(a="body");l.find("input").removeAttr("name");l.appendTo(a);this.s.heights.row=d("tr",e).eq(1).outerHeight();l.remove()},_draw:function(){var a=this,b=this.s.heights,c=this.dom.scroller.scrollTop,e=d(this.s.dt.nTable).height(),l=this.s.dt._iDisplayStart,n=this.s.dt._iDisplayLength,p=this.s.dt.fnRecordsDisplay();this.s.skip=!0;!this.s.dt.bSorted&&!this.s.dt.bFiltered||0!==l||this.s.dt._drawHold||(this.s.topRowFloat=0);c="jump"===this.s.scrollType?this._domain("virtualToPhysical",
|
||||
this.s.topRowFloat*b.row):c;this.s.baseScrollTop=c;this.s.baseRowTop=this.s.topRowFloat;var r=c-(this.s.topRowFloat-l)*b.row;0===l?r=0:l+n>=p&&(r=b.scroll-e);this.dom.table.style.top=r+"px";this.s.tableTop=r;this.s.tableBottom=e+this.s.tableTop;e=(c-this.s.tableTop)*this.s.boundaryScale;this.s.redrawTop=c-e;this.s.redrawBottom=c+e>b.scroll-b.viewport-b.row?b.scroll-b.viewport-b.row:c+e;this.s.skip=!1;this.s.dt.oFeatures.bStateSave&&null!==this.s.dt.oLoadedState&&"undefined"!=typeof this.s.dt.oLoadedState.scroller?
|
||||
((b=!this.s.dt.sAjaxSource&&!a.s.dt.ajax||this.s.dt.oFeatures.bServerSide?!1:!0)&&2==this.s.dt.iDraw||!b&&1==this.s.dt.iDraw)&&setTimeout(function(){d(a.dom.scroller).scrollTop(a.s.dt.oLoadedState.scroller.scrollTop);setTimeout(function(){a.s.ingnoreScroll=!1},0)},0):a.s.ingnoreScroll=!1;this.s.dt.oFeatures.bInfo&&setTimeout(function(){a._info.call(a)},0);this.dom.loader&&this.s.loaderVisible&&(this.dom.loader.css("display","none"),this.s.loaderVisible=!1)},_domain:function(a,b){var c=this.s.heights;
|
||||
if(c.virtual===c.scroll||1E4>b)return b;if("virtualToPhysical"===a&&b>=c.virtual-1E4)return a=c.virtual-b,c.scroll-a;if("physicalToVirtual"===a&&b>=c.scroll-1E4)return a=c.scroll-b,c.virtual-a;c=(c.virtual-1E4-1E4)/(c.scroll-1E4-1E4);var e=1E4-1E4*c;return"virtualToPhysical"===a?(b-e)/c:c*b+e},_info:function(){if(this.s.dt.oFeatures.bInfo){var a=this.s.dt,b=a.oLanguage,c=this.dom.scroller.scrollTop,e=Math.floor(this.pixelsToRow(c,!1,this.s.ani)+1),l=a.fnRecordsTotal(),n=a.fnRecordsDisplay();c=Math.ceil(this.pixelsToRow(c+
|
||||
this.s.heights.viewport,!1,this.s.ani));c=n<c?n:c;var p=a.fnFormatNumber(e),r=a.fnFormatNumber(c),t=a.fnFormatNumber(l),u=a.fnFormatNumber(n);p=0===a.fnRecordsDisplay()&&a.fnRecordsDisplay()==a.fnRecordsTotal()?b.sInfoEmpty+b.sInfoPostFix:0===a.fnRecordsDisplay()?b.sInfoEmpty+" "+b.sInfoFiltered.replace("_MAX_",t)+b.sInfoPostFix:a.fnRecordsDisplay()==a.fnRecordsTotal()?b.sInfo.replace("_START_",p).replace("_END_",r).replace("_MAX_",t).replace("_TOTAL_",u)+b.sInfoPostFix:b.sInfo.replace("_START_",
|
||||
p).replace("_END_",r).replace("_MAX_",t).replace("_TOTAL_",u)+" "+b.sInfoFiltered.replace("_MAX_",a.fnFormatNumber(a.fnRecordsTotal()))+b.sInfoPostFix;(b=b.fnInfoCallback)&&(p=b.call(a.oInstance,a,e,c,l,n,p));e=a.aanFeatures.i;if("undefined"!=typeof e)for(l=0,n=e.length;l<n;l++)d(e[l]).html(p);d(a.nTable).triggerHandler("info.dt")}},_parseHeight:function(a){var b,c=/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))(px|em|rem|vh)$/.exec(a);if(null===c)return 0;a=parseFloat(c[1]);c=c[2];"px"===c?b=a:"vh"===c?b=a/100*
|
||||
d(f).height():"rem"===c?b=a*parseFloat(d(":root").css("font-size")):"em"===c&&(b=a*parseFloat(d("body").css("font-size")));return b?b:0},_scroll:function(){var a=this,b=this.s.heights,c=this.dom.scroller.scrollTop;if(!this.s.skip&&!this.s.ingnoreScroll&&c!==this.s.lastScrollTop)if(this.s.dt.bFiltered||this.s.dt.bSorted)this.s.lastScrollTop=0;else{this._info();clearTimeout(this.s.stateTO);this.s.stateTO=setTimeout(function(){a.s.dtApi.state.save()},250);this.s.scrollType=Math.abs(c-this.s.lastScrollTop)>
|
||||
b.viewport?"jump":"cont";this.s.topRowFloat="cont"===this.s.scrollType?this.pixelsToRow(c,!1,!1):this._domain("physicalToVirtual",c)/b.row;0>this.s.topRowFloat&&(this.s.topRowFloat=0);if(this.s.forceReposition||c<this.s.redrawTop||c>this.s.redrawBottom){var e=Math.ceil((this.s.displayBuffer-1)/2*this.s.viewportRows);e=parseInt(this.s.topRowFloat,10)-e;this.s.forceReposition=!1;0>=e?e=0:e+this.s.dt._iDisplayLength>this.s.dt.fnRecordsDisplay()?(e=this.s.dt.fnRecordsDisplay()-this.s.dt._iDisplayLength,
|
||||
0>e&&(e=0)):0!==e%2&&e++;this.s.targetTop=e;e!=this.s.dt._iDisplayStart&&(this.s.tableTop=d(this.s.dt.nTable).offset().top,this.s.tableBottom=d(this.s.dt.nTable).height()+this.s.tableTop,e=function(){a.s.dt._iDisplayStart=a.s.targetTop;a.s.dt.oApi._fnDraw(a.s.dt)},this.s.dt.oFeatures.bServerSide?(this.s.forceReposition=!0,clearTimeout(this.s.drawTO),this.s.drawTO=setTimeout(e,this.s.serverWait)):e(),this.dom.loader&&!this.s.loaderVisible&&(this.dom.loader.css("display","block"),this.s.loaderVisible=
|
||||
!0))}else this.s.topRowFloat=this.pixelsToRow(c,!1,!0);this.s.lastScrollTop=c;this.s.stateSaveThrottle();"jump"===this.s.scrollType&&this.s.mousedown&&(this.s.labelVisible=!0);this.s.labelVisible&&this.dom.label.html(this.s.dt.fnFormatNumber(parseInt(this.s.topRowFloat,10)+1)).css("top",c+c*b.labelFactor).css("display","block")}},_scrollForce:function(){var a=this.s.heights;a.virtual=a.row*this.s.dt.fnRecordsDisplay();a.scroll=a.virtual;1E6<a.scroll&&(a.scroll=1E6);this.dom.force.style.height=a.scroll>
|
||||
this.s.heights.row?a.scroll+"px":this.s.heights.row+"px"}});m.defaults={boundaryScale:.5,displayBuffer:9,loadingIndicator:!1,rowHeight:"auto",serverWait:200};m.oDefaults=m.defaults;m.version="2.0.3";d(g).on("preInit.dt.dtscroller",function(a,b){if("dt"===a.namespace){a=b.oInit.scroller;var c=k.defaults.scroller;if(a||c)c=d.extend({},a,c),!1!==a&&new m(b,c)}});d.fn.dataTable.Scroller=m;d.fn.DataTable.Scroller=m;var q=d.fn.dataTable.Api;q.register("scroller()",function(){return this});q.register("scroller().rowToPixels()",
|
||||
function(a,b,c){var e=this.context;if(e.length&&e[0].oScroller)return e[0].oScroller.rowToPixels(a,b,c)});q.register("scroller().pixelsToRow()",function(a,b,c){var e=this.context;if(e.length&&e[0].oScroller)return e[0].oScroller.pixelsToRow(a,b,c)});q.register(["scroller().scrollToRow()","scroller.toPosition()"],function(a,b){this.iterator("table",function(c){c.oScroller&&c.oScroller.scrollToRow(a,b)});return this});q.register("row().scrollTo()",function(a){var b=this;this.iterator("row",function(c,
|
||||
e){c.oScroller&&(e=b.rows({order:"applied",search:"applied"}).indexes().indexOf(e),c.oScroller.scrollToRow(e,a))});return this});q.register("scroller.measure()",function(a){this.iterator("table",function(b){b.oScroller&&b.oScroller.measure(a)});return this});q.register("scroller.page()",function(){var a=this.context;if(a.length&&a[0].oScroller)return a[0].oScroller.pageInfo()});return m});
|
||||
|
||||
|
||||
/*!
|
||||
Bootstrap 4 styling wrapper for Scroller
|
||||
©2018 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-scroller"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.Scroller||require("datatables.net-scroller")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){return c.fn.dataTable});
|
||||
|
||||
|
||||
/*!
|
||||
SearchPanes 1.2.2
|
||||
2019-2020 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.getGlobal=function(m){m=["object"==typeof globalThis&&globalThis,m,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var t=0;t<m.length;++t){var h=m[t];if(h&&h.Math==Math)return h}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.checkEs6ConformanceViaProxy=function(){try{var m={},t=Object.create(new $jscomp.global.Proxy(m,{get:function(h,r,v){return h==m&&"q"==r&&v==t}}));return!0===t.q}catch(h){return!1}};$jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS=!1;$jscomp.ES6_CONFORMANCE=$jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS&&$jscomp.checkEs6ConformanceViaProxy();$jscomp.arrayIteratorImpl=function(m){var t=0;return function(){return t<m.length?{done:!1,value:m[t++]}:{done:!0}}};$jscomp.arrayIterator=function(m){return{next:$jscomp.arrayIteratorImpl(m)}};
|
||||
$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(m,t,h){if(m==Array.prototype||m==Object.prototype)return m;m[t]=h.value;return m};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;
|
||||
$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(m,t){var h=$jscomp.propertyToPolyfillSymbol[t];if(null==h)return m[t];h=m[h];return void 0!==h?h:m[t]};$jscomp.polyfill=function(m,t,h,r){t&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(m,t,h,r):$jscomp.polyfillUnisolated(m,t,h,r))};
|
||||
$jscomp.polyfillUnisolated=function(m,t,h,r){h=$jscomp.global;m=m.split(".");for(r=0;r<m.length-1;r++){var v=m[r];if(!(v in h))return;h=h[v]}m=m[m.length-1];r=h[m];t=t(r);t!=r&&null!=t&&$jscomp.defineProperty(h,m,{configurable:!0,writable:!0,value:t})};
|
||||
$jscomp.polyfillIsolated=function(m,t,h,r){var v=m.split(".");m=1===v.length;r=v[0];r=!m&&r in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var q=0;q<v.length-1;q++){var A=v[q];if(!(A in r))return;r=r[A]}v=v[v.length-1];h=$jscomp.IS_SYMBOL_NATIVE&&"es6"===h?r[v]:null;t=t(h);null!=t&&(m?$jscomp.defineProperty($jscomp.polyfills,v,{configurable:!0,writable:!0,value:t}):t!==h&&($jscomp.propertyToPolyfillSymbol[v]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(v):$jscomp.POLYFILL_PREFIX+v,v=
|
||||
$jscomp.propertyToPolyfillSymbol[v],$jscomp.defineProperty(r,v,{configurable:!0,writable:!0,value:t})))};$jscomp.initSymbol=function(){};
|
||||
$jscomp.polyfill("Symbol",function(m){if(m)return m;var t=function(v,q){this.$jscomp$symbol$id_=v;$jscomp.defineProperty(this,"description",{configurable:!0,writable:!0,value:q})};t.prototype.toString=function(){return this.$jscomp$symbol$id_};var h=0,r=function(v){if(this instanceof r)throw new TypeError("Symbol is not a constructor");return new t("jscomp_symbol_"+(v||"")+"_"+h++,v)};return r},"es6","es3");$jscomp.initSymbolIterator=function(){};
|
||||
$jscomp.polyfill("Symbol.iterator",function(m){if(m)return m;m=Symbol("Symbol.iterator");for(var t="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),h=0;h<t.length;h++){var r=$jscomp.global[t[h]];"function"===typeof r&&"function"!=typeof r.prototype[m]&&$jscomp.defineProperty(r.prototype,m,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}})}return m},"es6",
|
||||
"es3");$jscomp.initSymbolAsyncIterator=function(){};$jscomp.iteratorPrototype=function(m){m={next:m};m[Symbol.iterator]=function(){return this};return m};$jscomp.makeIterator=function(m){var t="undefined"!=typeof Symbol&&Symbol.iterator&&m[Symbol.iterator];return t?t.call(m):$jscomp.arrayIterator(m)};$jscomp.owns=function(m,t){return Object.prototype.hasOwnProperty.call(m,t)};
|
||||
$jscomp.polyfill("WeakMap",function(m){function t(){if(!m||!Object.seal)return!1;try{var a=Object.seal({}),b=Object.seal({}),c=new m([[a,2],[b,3]]);if(2!=c.get(a)||3!=c.get(b))return!1;c.delete(a);c.set(b,4);return!c.has(a)&&4==c.get(b)}catch(d){return!1}}function h(){}function r(a){var b=typeof a;return"object"===b&&null!==a||"function"===b}function v(a){if(!$jscomp.owns(a,A)){var b=new h;$jscomp.defineProperty(a,A,{value:b})}}function q(a){if(!$jscomp.ISOLATE_POLYFILLS){var b=Object[a];b&&(Object[a]=
|
||||
function(c){if(c instanceof h)return c;Object.isExtensible(c)&&v(c);return b(c)})}}if($jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS){if(m&&$jscomp.ES6_CONFORMANCE)return m}else if(t())return m;var A="$jscomp_hidden_"+Math.random();q("freeze");q("preventExtensions");q("seal");var G=0,k=function(a){this.id_=(G+=Math.random()+1).toString();if(a){a=$jscomp.makeIterator(a);for(var b;!(b=a.next()).done;)b=b.value,this.set(b[0],b[1])}};k.prototype.set=function(a,b){if(!r(a))throw Error("Invalid WeakMap key");
|
||||
v(a);if(!$jscomp.owns(a,A))throw Error("WeakMap key fail: "+a);a[A][this.id_]=b;return this};k.prototype.get=function(a){return r(a)&&$jscomp.owns(a,A)?a[A][this.id_]:void 0};k.prototype.has=function(a){return r(a)&&$jscomp.owns(a,A)&&$jscomp.owns(a[A],this.id_)};k.prototype.delete=function(a){return r(a)&&$jscomp.owns(a,A)&&$jscomp.owns(a[A],this.id_)?delete a[A][this.id_]:!1};return k},"es6","es3");$jscomp.MapEntry=function(){};
|
||||
$jscomp.polyfill("Map",function(m){function t(){if($jscomp.ASSUME_NO_NATIVE_MAP||!m||"function"!=typeof m||!m.prototype.entries||"function"!=typeof Object.seal)return!1;try{var k=Object.seal({x:4}),a=new m($jscomp.makeIterator([[k,"s"]]));if("s"!=a.get(k)||1!=a.size||a.get({x:4})||a.set({x:4},"t")!=a||2!=a.size)return!1;var b=a.entries(),c=b.next();if(c.done||c.value[0]!=k||"s"!=c.value[1])return!1;c=b.next();return c.done||4!=c.value[0].x||"t"!=c.value[1]||!b.next().done?!1:!0}catch(d){return!1}}
|
||||
if($jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS){if(m&&$jscomp.ES6_CONFORMANCE)return m}else if(t())return m;var h=new WeakMap,r=function(k){this.data_={};this.head_=A();this.size=0;if(k){k=$jscomp.makeIterator(k);for(var a;!(a=k.next()).done;)a=a.value,this.set(a[0],a[1])}};r.prototype.set=function(k,a){k=0===k?0:k;var b=v(this,k);b.list||(b.list=this.data_[b.id]=[]);b.entry?b.entry.value=a:(b.entry={next:this.head_,previous:this.head_.previous,head:this.head_,key:k,value:a},b.list.push(b.entry),
|
||||
this.head_.previous.next=b.entry,this.head_.previous=b.entry,this.size++);return this};r.prototype.delete=function(k){k=v(this,k);return k.entry&&k.list?(k.list.splice(k.index,1),k.list.length||delete this.data_[k.id],k.entry.previous.next=k.entry.next,k.entry.next.previous=k.entry.previous,k.entry.head=null,this.size--,!0):!1};r.prototype.clear=function(){this.data_={};this.head_=this.head_.previous=A();this.size=0};r.prototype.has=function(k){return!!v(this,k).entry};r.prototype.get=function(k){return(k=
|
||||
v(this,k).entry)&&k.value};r.prototype.entries=function(){return q(this,function(k){return[k.key,k.value]})};r.prototype.keys=function(){return q(this,function(k){return k.key})};r.prototype.values=function(){return q(this,function(k){return k.value})};r.prototype.forEach=function(k,a){for(var b=this.entries(),c;!(c=b.next()).done;)c=c.value,k.call(a,c[1],c[0],this)};r.prototype[Symbol.iterator]=r.prototype.entries;var v=function(k,a){var b=a&&typeof a;"object"==b||"function"==b?h.has(a)?b=h.get(a):
|
||||
(b=""+ ++G,h.set(a,b)):b="p_"+a;var c=k.data_[b];if(c&&$jscomp.owns(k.data_,b))for(k=0;k<c.length;k++){var d=c[k];if(a!==a&&d.key!==d.key||a===d.key)return{id:b,list:c,index:k,entry:d}}return{id:b,list:c,index:-1,entry:void 0}},q=function(k,a){var b=k.head_;return $jscomp.iteratorPrototype(function(){if(b){for(;b.head!=k.head_;)b=b.previous;for(;b.next!=b.head;)return b=b.next,{done:!1,value:a(b)};b=null}return{done:!0,value:void 0}})},A=function(){var k={};return k.previous=k.next=k.head=k},G=0;
|
||||
return r},"es6","es3");$jscomp.findInternal=function(m,t,h){m instanceof String&&(m=String(m));for(var r=m.length,v=0;v<r;v++){var q=m[v];if(t.call(h,q,v,m))return{i:v,v:q}}return{i:-1,v:void 0}};$jscomp.polyfill("Array.prototype.find",function(m){return m?m:function(t,h){return $jscomp.findInternal(this,t,h).v}},"es6","es3");
|
||||
$jscomp.iteratorFromArray=function(m,t){m instanceof String&&(m+="");var h=0,r={next:function(){if(h<m.length){var v=h++;return{value:t(v,m[v]),done:!1}}r.next=function(){return{done:!0,value:void 0}};return r.next()}};r[Symbol.iterator]=function(){return r};return r};$jscomp.polyfill("Array.prototype.keys",function(m){return m?m:function(){return $jscomp.iteratorFromArray(this,function(t){return t})}},"es6","es3");
|
||||
$jscomp.polyfill("Array.prototype.findIndex",function(m){return m?m:function(t,h){return $jscomp.findInternal(this,t,h).i}},"es6","es3");
|
||||
(function(){function m(k){h=k;r=k.fn.dataTable}function t(k){q=k;A=k.fn.dataTable}var h,r,v=function(){function k(a,b,c,d,e,g){var f=this;void 0===g&&(g=null);if(!r||!r.versionCheck||!r.versionCheck("1.10.0"))throw Error("SearchPane requires DataTables 1.10 or newer");if(!r.select)throw Error("SearchPane requires Select");a=new r.Api(a);this.classes=h.extend(!0,{},k.classes);this.c=h.extend(!0,{},k.defaults,b);this.customPaneSettings=g;this.s={cascadeRegen:!1,clearing:!1,colOpts:[],deselect:!1,displayed:!1,
|
||||
dt:a,dtPane:void 0,filteringActive:!1,index:c,indexes:[],lastCascade:!1,lastSelect:!1,listSet:!1,name:void 0,redraw:!1,rowData:{arrayFilter:[],arrayOriginal:[],arrayTotals:[],bins:{},binsOriginal:{},binsTotal:{},filterMap:new Map,totalOptions:0},scrollTop:0,searchFunction:void 0,selectPresent:!1,serverSelect:[],serverSelecting:!1,showFiltered:!1,tableLength:null,updating:!1};b=a.columns().eq(0).toArray().length;this.colExists=this.s.index<b;this.c.layout=d;b=parseInt(d.split("-")[1],10);this.dom=
|
||||
{buttonGroup:h("<div/>").addClass(this.classes.buttonGroup),clear:h('<button type="button">×</button>').addClass(this.classes.dull).addClass(this.classes.paneButton).addClass(this.classes.clearButton),container:h("<div/>").addClass(this.classes.container).addClass(this.classes.layout+(10>b?d:d.split("-")[0]+"-9")),countButton:h('<button type="button"></button>').addClass(this.classes.paneButton).addClass(this.classes.countButton),dtP:h("<table><thead><tr><th>"+(this.colExists?h(a.column(this.colExists?
|
||||
this.s.index:0).header()).text():this.customPaneSettings.header||"Custom Pane")+"</th><th/></tr></thead></table>"),lower:h("<div/>").addClass(this.classes.subRow2).addClass(this.classes.narrowButton),nameButton:h('<button type="button"></button>').addClass(this.classes.paneButton).addClass(this.classes.nameButton),panesContainer:e,searchBox:h("<input/>").addClass(this.classes.paneInputButton).addClass(this.classes.search),searchButton:h('<button type = "button" class="'+this.classes.searchIcon+'"></button>').addClass(this.classes.paneButton),
|
||||
searchCont:h("<div/>").addClass(this.classes.searchCont),searchLabelCont:h("<div/>").addClass(this.classes.searchLabelCont),topRow:h("<div/>").addClass(this.classes.topRow),upper:h("<div/>").addClass(this.classes.subRow1).addClass(this.classes.narrowSearch)};this.s.displayed=!1;a=this.s.dt;this.selections=[];this.s.colOpts=this.colExists?this._getOptions():this._getBonusOptions();var l=this.s.colOpts;d=h('<button type="button">X</button>').addClass(this.classes.paneButton);h(d).text(a.i18n("searchPanes.clearPane",
|
||||
"X"));this.dom.container.addClass(l.className);this.dom.container.addClass(null!==this.customPaneSettings&&void 0!==this.customPaneSettings.className?this.customPaneSettings.className:"");this.s.name=void 0!==this.s.colOpts.name?this.s.colOpts.name:null!==this.customPaneSettings&&void 0!==this.customPaneSettings.name?this.customPaneSettings.name:this.colExists?h(a.column(this.s.index).header()).text():this.customPaneSettings.header||"Custom Pane";h(e).append(this.dom.container);var p=a.table(0).node();
|
||||
this.s.searchFunction=function(n,x,z,y){if(0===f.selections.length||n.nTable!==p)return!0;n=null;f.colExists&&(n=x[f.s.index],"filter"!==l.orthogonal.filter&&(n=f.s.rowData.filterMap.get(z),n instanceof h.fn.dataTable.Api&&(n=n.toArray())));return f._search(n,z)};h.fn.dataTable.ext.search.push(this.s.searchFunction);if(this.c.clear)h(d).on("click",function(){f.dom.container.find(f.classes.search).each(function(){h(this).val("");h(this).trigger("input")});f.clearPane()});a.on("draw.dtsp",function(){f._adjustTopRow()});
|
||||
a.on("buttons-action",function(){f._adjustTopRow()});h(window).on("resize.dtsp",r.util.throttle(function(){f._adjustTopRow()}));a.on("column-reorder.dtsp",function(n,x,z){f.s.index=z.mapping[f.s.index]});return this}k.prototype.clearData=function(){this.s.rowData={arrayFilter:[],arrayOriginal:[],arrayTotals:[],bins:{},binsOriginal:{},binsTotal:{},filterMap:new Map,totalOptions:0}};k.prototype.clearPane=function(){this.s.dtPane.rows({selected:!0}).deselect();this.updateTable();return this};k.prototype.destroy=
|
||||
function(){h(this.s.dtPane).off(".dtsp");h(this.s.dt).off(".dtsp");h(this.dom.nameButton).off(".dtsp");h(this.dom.countButton).off(".dtsp");h(this.dom.clear).off(".dtsp");h(this.dom.searchButton).off(".dtsp");h(this.dom.container).remove();for(var a=h.fn.dataTable.ext.search.indexOf(this.s.searchFunction);-1!==a;)h.fn.dataTable.ext.search.splice(a,1),a=h.fn.dataTable.ext.search.indexOf(this.s.searchFunction);void 0!==this.s.dtPane&&this.s.dtPane.destroy();this.s.listSet=!1};k.prototype.getPaneCount=
|
||||
function(){return void 0!==this.s.dtPane?this.s.dtPane.rows({selected:!0}).data().toArray().length:0};k.prototype.rebuildPane=function(a,b,c,d){void 0===a&&(a=!1);void 0===b&&(b=null);void 0===c&&(c=null);void 0===d&&(d=!1);this.clearData();var e=[];this.s.serverSelect=[];var g=null;void 0!==this.s.dtPane&&(d&&(this.s.dt.page.info().serverSide?this.s.serverSelect=this.s.dtPane.rows({selected:!0}).data().toArray():e=this.s.dtPane.rows({selected:!0}).data().toArray()),this.s.dtPane.clear().destroy(),
|
||||
g=h(this.dom.container).prev(),this.destroy(),this.s.dtPane=void 0,h.fn.dataTable.ext.search.push(this.s.searchFunction));this.dom.container.removeClass(this.classes.hidden);this.s.displayed=!1;this._buildPane(this.s.dt.page.info().serverSide?this.s.serverSelect:e,a,b,c,g);return this};k.prototype.removePane=function(){this.s.displayed=!1;h(this.dom.container).hide()};k.prototype.setCascadeRegen=function(a){this.s.cascadeRegen=a};k.prototype.setClear=function(a){this.s.clearing=a};k.prototype.updatePane=
|
||||
function(a){void 0===a&&(a=!1);this.s.updating=!0;this._updateCommon(a);this.s.updating=!1};k.prototype.updateTable=function(){this.selections=this.s.dtPane.rows({selected:!0}).data().toArray();this._searchExtras();(this.c.cascadePanes||this.c.viewTotal)&&this.updatePane()};k.prototype._setListeners=function(){var a=this,b=this.s.rowData,c;this.s.dtPane.on("select.dtsp",function(){clearTimeout(c);a.s.dt.page.info().serverSide&&!a.s.updating?a.s.serverSelecting||(a.s.serverSelect=a.s.dtPane.rows({selected:!0}).data().toArray(),
|
||||
a.s.scrollTop=h(a.s.dtPane.table().node()).parent()[0].scrollTop,a.s.selectPresent=!0,a.s.dt.draw(!1)):(h(a.dom.clear).removeClass(a.classes.dull),a.s.selectPresent=!0,a.s.updating||a._makeSelection(),a.s.selectPresent=!1)});this.s.dtPane.on("deselect.dtsp",function(){c=setTimeout(function(){a.s.dt.page.info().serverSide&&!a.s.updating?a.s.serverSelecting||(a.s.serverSelect=a.s.dtPane.rows({selected:!0}).data().toArray(),a.s.deselect=!0,a.s.dt.draw(!1)):(a.s.deselect=!0,0===a.s.dtPane.rows({selected:!0}).data().toArray().length&&
|
||||
h(a.dom.clear).addClass(a.classes.dull),a._makeSelection(),a.s.deselect=!1,a.s.dt.state.save())},50)});this.s.dt.on("stateSaveParams.dtsp",function(d,e,g){if(h.isEmptyObject(g))a.s.dtPane.state.clear();else{d=[];if(void 0!==a.s.dtPane){d=a.s.dtPane.rows({selected:!0}).data().map(function(x){return x.filter.toString()}).toArray();var f=h(a.dom.searchBox).val();var l=a.s.dtPane.order();var p=b.binsOriginal;var n=b.arrayOriginal}void 0===g.searchPanes&&(g.searchPanes={});void 0===g.searchPanes.panes&&
|
||||
(g.searchPanes.panes=[]);for(e=0;e<g.searchPanes.panes.length;e++)g.searchPanes.panes[e].id===a.s.index&&(g.searchPanes.panes.splice(e,1),e--);g.searchPanes.panes.push({arrayFilter:n,bins:p,id:a.s.index,order:l,searchTerm:f,selected:d})}});this.s.dtPane.on("user-select.dtsp",function(d,e,g,f,l){l.stopPropagation()});this.s.dtPane.on("draw.dtsp",function(){a._adjustTopRow()});h(this.dom.nameButton).on("click.dtsp",function(){var d=a.s.dtPane.order()[0][1];a.s.dtPane.order([0,"asc"===d?"desc":"asc"]).draw();
|
||||
a.s.dt.state.save()});h(this.dom.countButton).on("click.dtsp",function(){var d=a.s.dtPane.order()[0][1];a.s.dtPane.order([1,"asc"===d?"desc":"asc"]).draw();a.s.dt.state.save()});h(this.dom.clear).on("click.dtsp",function(){a.dom.container.find("."+a.classes.search).each(function(){h(this).val("");h(this).trigger("input")});a.clearPane()});h(this.dom.searchButton).on("click.dtsp",function(){h(a.dom.searchBox).focus()});h(this.dom.searchBox).on("input.dtsp",function(){a.s.dtPane.search(h(a.dom.searchBox).val()).draw();
|
||||
a.s.dt.state.save()});this.s.dt.state.save();return!0};k.prototype._addOption=function(a,b,c,d,e,g){if(Array.isArray(a)||a instanceof r.Api)if(a instanceof r.Api&&(a=a.toArray(),b=b.toArray()),a.length===b.length)for(var f=0;f<a.length;f++)g[a[f]]?g[a[f]]++:(g[a[f]]=1,e.push({display:b[f],filter:a[f],sort:c[f],type:d[f]})),this.s.rowData.totalOptions++;else throw Error("display and filter not the same length");else"string"===typeof this.s.colOpts.orthogonal?(g[a]?g[a]++:(g[a]=1,e.push({display:b,
|
||||
filter:a,sort:c,type:d})),this.s.rowData.totalOptions++):e.push({display:b,filter:a,sort:c,type:d})};k.prototype._addRow=function(a,b,c,d,e,g,f){for(var l,p=0,n=this.s.indexes;p<n.length;p++){var x=n[p];x.filter===b&&(l=x.index)}void 0===l&&(l=this.s.indexes.length,this.s.indexes.push({filter:b,index:l}));return this.s.dtPane.row.add({className:f,display:""!==a?a:!1!==this.s.colOpts.emptyMessage?this.s.colOpts.emptyMessage:this.c.emptyMessage,filter:b,index:l,shown:c,sort:""!==e?e:!1!==this.s.colOpts.emptyMessage?
|
||||
this.s.colOpts.emptyMessage:this.c.emptyMessage,total:d,type:g})};k.prototype._adjustTopRow=function(){var a=this.dom.container.find("."+this.classes.subRowsContainer),b=this.dom.container.find(".dtsp-subRow1"),c=this.dom.container.find(".dtsp-subRow2"),d=this.dom.container.find("."+this.classes.topRow);(252>h(a[0]).width()||252>h(d[0]).width())&&0!==h(a[0]).width()?(h(a[0]).addClass(this.classes.narrow),h(b[0]).addClass(this.classes.narrowSub).removeClass(this.classes.narrowSearch),h(c[0]).addClass(this.classes.narrowSub).removeClass(this.classes.narrowButton)):
|
||||
(h(a[0]).removeClass(this.classes.narrow),h(b[0]).removeClass(this.classes.narrowSub).addClass(this.classes.narrowSearch),h(c[0]).removeClass(this.classes.narrowSub).addClass(this.classes.narrowButton))};k.prototype._buildPane=function(a,b,c,d,e){var g=this;void 0===a&&(a=[]);void 0===b&&(b=!1);void 0===c&&(c=null);void 0===d&&(d=null);void 0===e&&(e=null);this.selections=[];var f=this.s.dt,l=f.column(this.colExists?this.s.index:0),p=this.s.colOpts,n=this.s.rowData,x=f.i18n("searchPanes.count","{total}"),
|
||||
z=f.i18n("searchPanes.countFiltered","{shown} ({total})"),y=f.state.loaded();this.s.listSet&&(y=f.state());if(this.colExists){var w=-1;if(y&&y.searchPanes&&y.searchPanes.panes)for(var u=0;u<y.searchPanes.panes.length;u++)if(y.searchPanes.panes[u].id===this.s.index){w=u;break}if((!1===p.show||void 0!==p.show&&!0!==p.show)&&-1===w)return this.dom.container.addClass(this.classes.hidden),this.s.displayed=!1;if(!0===p.show||-1!==w)this.s.displayed=!0;if(!this.s.dt.page.info().serverSide&&(null===c||null===
|
||||
c.searchPanes||null===c.searchPanes.options)){if(0===n.arrayFilter.length){this._populatePane(b);this.s.rowData.totalOptions=0;this._detailsPane();if(y&&y.searchPanes&&y.searchPanes.panes&&-1===w){this.dom.container.addClass(this.classes.hidden);this.s.displayed=!1;return}n.arrayOriginal=n.arrayTotals;n.binsOriginal=n.binsTotal}u=Object.keys(n.binsOriginal).length;b=this._uniqueRatio(u,f.rows()[0].length);if(!1===this.s.displayed&&((void 0===p.show&&null===p.threshold?b>this.c.threshold:b>p.threshold)||
|
||||
!0!==p.show&&1>=u)){this.dom.container.addClass(this.classes.hidden);this.s.displayed=!1;return}this.c.viewTotal&&0===n.arrayTotals.length?(this.s.rowData.totalOptions=0,this._detailsPane()):n.binsTotal=n.bins;this.dom.container.addClass(this.classes.show);this.s.displayed=!0}else if(null!==c&&null!==c.searchPanes&&null!==c.searchPanes.options){if(void 0!==c.tableLength)this.s.tableLength=c.tableLength,this.s.rowData.totalOptions=this.s.tableLength;else if(null===this.s.tableLength||f.rows()[0].length>
|
||||
this.s.tableLength)this.s.tableLength=f.rows()[0].length,this.s.rowData.totalOptions=this.s.tableLength;b=f.column(this.s.index).dataSrc();if(void 0!==c.searchPanes.options[b])for(u=0,b=c.searchPanes.options[b];u<b.length;u++)w=b[u],this.s.rowData.arrayFilter.push({display:w.label,filter:w.value,sort:w.label,type:w.label}),this.s.rowData.bins[w.value]=this.c.viewTotal||this.c.cascadePanes?w.count:w.total,this.s.rowData.binsTotal[w.value]=w.total;u=Object.keys(n.binsTotal).length;b=this._uniqueRatio(u,
|
||||
this.s.tableLength);if(!1===this.s.displayed&&((void 0===p.show&&null===p.threshold?b>this.c.threshold:b>p.threshold)||!0!==p.show&&1>=u)){this.dom.container.addClass(this.classes.hidden);this.s.displayed=!1;return}this.s.rowData.arrayOriginal=this.s.rowData.arrayFilter;this.s.rowData.binsOriginal=this.s.rowData.bins;this.s.displayed=!0}}else this.s.displayed=!0;this._displayPane();if(!this.s.listSet)this.dom.dtP.on("stateLoadParams.dt",function(E,F,D){h.isEmptyObject(f.state.loaded())&&h.each(D,
|
||||
function(C,I){delete D[C]})});null!==e&&0<h(this.dom.panesContainer).has(e).length?h(this.dom.container).insertAfter(e):h(this.dom.panesContainer).prepend(this.dom.container);u=h.fn.dataTable.ext.errMode;h.fn.dataTable.ext.errMode="none";e=r.Scroller;this.s.dtPane=h(this.dom.dtP).DataTable(h.extend(!0,{columnDefs:[{className:"dtsp-nameColumn",data:"display",render:function(E,F,D){if("sort"===F)return D.sort;if("type"===F)return D.type;var C;(g.s.filteringActive||g.s.showFiltered)&&g.c.viewTotal?C=
|
||||
z.replace(/{total}/,D.total):C=x.replace(/{total}/,D.total);for(C=C.replace(/{shown}/,D.shown);-1!==C.indexOf("{total}");)C=C.replace(/{total}/,D.total);for(;-1!==C.indexOf("{shown}");)C=C.replace(/{shown}/,D.shown);F='<span class="'+g.classes.pill+'">'+C+"</span>";if(g.c.hideCount||p.hideCount)F="";return'<div class="'+g.classes.nameCont+'"><span title="'+("string"===typeof E&&null!==E.match(/<[^>]*>/)?E.replace(/<[^>]*>/g,""):E)+'" class="'+g.classes.name+'">'+E+"</span>"+F+"</div>"},targets:0,
|
||||
type:void 0!==f.settings()[0].aoColumns[this.s.index]?f.settings()[0].aoColumns[this.s.index]._sManualType:null},{className:"dtsp-countColumn "+this.classes.badgePill,data:"shown",orderData:[1,2],targets:1,visible:!1},{data:"total",targets:2,visible:!1}],deferRender:!0,dom:"t",info:!1,language:this.s.dt.settings()[0].oLanguage,paging:e?!0:!1,scrollX:!1,scrollY:"200px",scroller:e?!0:!1,select:!0,stateSave:f.settings()[0].oFeatures.bStateSave?!0:!1},this.c.dtOpts,void 0!==p?p.dtOpts:{},void 0===this.s.colOpts.options&&
|
||||
this.colExists?void 0:{createdRow:function(E,F,D){h(E).addClass(F.className)}},null!==this.customPaneSettings&&void 0!==this.customPaneSettings.dtOpts?this.customPaneSettings.dtOpts:{}));h(this.dom.dtP).addClass(this.classes.table);h(this.dom.searchBox).attr("placeholder",void 0!==p.header?p.header:this.colExists?f.settings()[0].aoColumns[this.s.index].sTitle:this.customPaneSettings.header||"Custom Pane");h.fn.dataTable.select.init(this.s.dtPane);h.fn.dataTable.ext.errMode=u;if(this.colExists){l=
|
||||
(l=l.search())?l.substr(1,l.length-2).split("|"):[];var B=0;n.arrayFilter.forEach(function(E){""===E.filter&&B++});u=0;for(e=n.arrayFilter.length;u<e;u++){l=!1;w=0;for(var H=this.s.serverSelect;w<H.length;w++)b=H[w],b.filter===n.arrayFilter[u].filter&&(l=!0);if(this.s.dt.page.info().serverSide&&(!this.c.cascadePanes||this.c.cascadePanes&&0!==n.bins[n.arrayFilter[u].filter]||this.c.cascadePanes&&null!==d||l))for(l=this._addRow(n.arrayFilter[u].display,n.arrayFilter[u].filter,d?n.binsTotal[n.arrayFilter[u].filter]:
|
||||
n.bins[n.arrayFilter[u].filter],this.c.viewTotal||d?String(n.binsTotal[n.arrayFilter[u].filter]):n.bins[n.arrayFilter[u].filter],n.arrayFilter[u].sort,n.arrayFilter[u].type),w=0,H=this.s.serverSelect;w<H.length;w++)b=H[w],b.filter===n.arrayFilter[u].filter&&(this.s.serverSelecting=!0,l.select(),this.s.serverSelecting=!1);else this.s.dt.page.info().serverSide||!n.arrayFilter[u]||void 0===n.bins[n.arrayFilter[u].filter]&&this.c.cascadePanes?this.s.dt.page.info().serverSide||this._addRow("",B,B,"","",
|
||||
""):this._addRow(n.arrayFilter[u].display,n.arrayFilter[u].filter,n.bins[n.arrayFilter[u].filter],n.binsTotal[n.arrayFilter[u].filter],n.arrayFilter[u].sort,n.arrayFilter[u].type)}}r.select.init(this.s.dtPane);(void 0!==p.options||null!==this.customPaneSettings&&void 0!==this.customPaneSettings.options)&&this._getComparisonRows();this.s.dtPane.draw();this._adjustTopRow();this.s.listSet||(this._setListeners(),this.s.listSet=!0);for(d=0;d<a.length;d++)if(n=a[d],void 0!==n)for(u=0,e=this.s.dtPane.rows().indexes().toArray();u<
|
||||
e.length;u++)l=e[u],void 0!==this.s.dtPane.row(l).data()&&n.filter===this.s.dtPane.row(l).data().filter&&(this.s.dt.page.info().serverSide?(this.s.serverSelecting=!0,this.s.dtPane.row(l).select(),this.s.serverSelecting=!1):this.s.dtPane.row(l).select());this.s.dt.page.info().serverSide&&this.s.dtPane.search(h(this.dom.searchBox).val()).draw();if(y&&y.searchPanes&&y.searchPanes.panes&&(null===c||1===c.draw))for(this.c.cascadePanes||this._reloadSelect(y),c=0,y=y.searchPanes.panes;c<y.length;c++)a=y[c],
|
||||
a.id===this.s.index&&(h(this.dom.searchBox).val(a.searchTerm),h(this.dom.searchBox).trigger("input"),this.s.dtPane.order(a.order).draw());this.s.dt.state.save();return!0};k.prototype._detailsPane=function(){var a=this.s.dt;this.s.rowData.arrayTotals=[];this.s.rowData.binsTotal={};var b=this.s.dt.settings()[0];a=a.rows().indexes();if(!this.s.dt.page.info().serverSide)for(var c=0;c<a.length;c++)this._populatePaneArray(a[c],this.s.rowData.arrayTotals,b,this.s.rowData.binsTotal)};k.prototype._displayPane=
|
||||
function(){var a=this.dom.container,b=this.s.colOpts,c=parseInt(this.c.layout.split("-")[1],10);h(this.dom.topRow).empty();h(this.dom.dtP).empty();h(this.dom.topRow).addClass(this.classes.topRow);3<c&&h(this.dom.container).addClass(this.classes.smallGap);h(this.dom.topRow).addClass(this.classes.subRowsContainer);h(this.dom.upper).appendTo(this.dom.topRow);h(this.dom.lower).appendTo(this.dom.topRow);h(this.dom.searchCont).appendTo(this.dom.upper);h(this.dom.buttonGroup).appendTo(this.dom.lower);(!1===
|
||||
this.c.dtOpts.searching||void 0!==b.dtOpts&&!1===b.dtOpts.searching||!this.c.controls||!b.controls||null!==this.customPaneSettings&&void 0!==this.customPaneSettings.dtOpts&&void 0!==this.customPaneSettings.dtOpts.searching&&!this.customPaneSettings.dtOpts.searching)&&h(this.dom.searchBox).attr("disabled","disabled").removeClass(this.classes.paneInputButton).addClass(this.classes.disabledButton);h(this.dom.searchBox).appendTo(this.dom.searchCont);this._searchContSetup();this.c.clear&&this.c.controls&&
|
||||
b.controls&&h(this.dom.clear).appendTo(this.dom.buttonGroup);this.c.orderable&&b.orderable&&this.c.controls&&b.controls&&h(this.dom.nameButton).appendTo(this.dom.buttonGroup);!this.c.hideCount&&!b.hideCount&&this.c.orderable&&b.orderable&&this.c.controls&&b.controls&&h(this.dom.countButton).appendTo(this.dom.buttonGroup);h(this.dom.topRow).prependTo(this.dom.container);h(a).append(this.dom.dtP);h(a).show()};k.prototype._getBonusOptions=function(){return h.extend(!0,{},k.defaults,{orthogonal:{threshold:null},
|
||||
threshold:null},void 0!==this.c?this.c:{})};k.prototype._getComparisonRows=function(){var a=this.s.colOpts;a=void 0!==a.options?a.options:null!==this.customPaneSettings&&void 0!==this.customPaneSettings.options?this.customPaneSettings.options:void 0;if(void 0!==a){var b=this.s.dt.rows({search:"applied"}).data().toArray(),c=this.s.dt.rows({search:"applied"}),d=this.s.dt.rows().data().toArray(),e=this.s.dt.rows(),g=[];this.s.dtPane.clear();for(var f=0;f<a.length;f++){var l=a[f],p=""!==l.label?l.label:
|
||||
this.c.emptyMessage,n=l.className,x=p,z="function"===typeof l.value?l.value:[],y=0,w=p,u=0;if("function"===typeof l.value){for(var B=0;B<b.length;B++)l.value.call(this.s.dt,b[B],c[0][B])&&y++;for(B=0;B<d.length;B++)l.value.call(this.s.dt,d[B],e[0][B])&&u++;"function"!==typeof z&&z.push(l.filter)}(!this.c.cascadePanes||this.c.cascadePanes&&0!==y)&&g.push(this._addRow(x,z,y,u,w,p,n))}return g}};k.prototype._getOptions=function(){return h.extend(!0,{},k.defaults,{emptyMessage:!1,orthogonal:{threshold:null},
|
||||
threshold:null},this.s.dt.settings()[0].aoColumns[this.s.index].searchPanes)};k.prototype._makeSelection=function(){this.updateTable();this.s.updating=!0;this.s.dt.draw();this.s.updating=!1};k.prototype._populatePane=function(a){void 0===a&&(a=!1);var b=this.s.dt;this.s.rowData.arrayFilter=[];this.s.rowData.bins={};var c=this.s.dt.settings()[0];if(!this.s.dt.page.info().serverSide){var d=0;for(a=(!this.c.cascadePanes&&!this.c.viewTotal||this.s.clearing||a?b.rows().indexes():b.rows({search:"applied"}).indexes()).toArray();d<
|
||||
a.length;d++)this._populatePaneArray(a[d],this.s.rowData.arrayFilter,c)}};k.prototype._populatePaneArray=function(a,b,c,d){void 0===d&&(d=this.s.rowData.bins);var e=this.s.colOpts;if("string"===typeof e.orthogonal)c=c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal),this.s.rowData.filterMap.set(a,c),this._addOption(c,c,c,c,b,d);else{var g=c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.search);null===g&&(g="");"string"===typeof g&&(g=g.replace(/<[^>]*>/g,""));this.s.rowData.filterMap.set(a,
|
||||
g);d[g]?d[g]++:(d[g]=1,this._addOption(g,c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.display),c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.sort),c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.type),b,d));this.s.rowData.totalOptions++}};k.prototype._reloadSelect=function(a){if(void 0!==a){for(var b,c=0;c<a.searchPanes.panes.length;c++)if(a.searchPanes.panes[c].id===this.s.index){b=c;break}if(void 0!==b){c=this.s.dtPane;var d=c.rows({order:"index"}).data().map(function(f){return null!==
|
||||
f.filter?f.filter.toString():null}).toArray(),e=0;for(a=a.searchPanes.panes[b].selected;e<a.length;e++){b=a[e];var g=-1;null!==b&&(g=d.indexOf(b.toString()));-1<g&&(this.s.serverSelecting=!0,c.row(g).select(),this.s.serverSelecting=!1)}}}};k.prototype._search=function(a,b){for(var c=this.s.colOpts,d=this.s.dt,e=0,g=this.selections;e<g.length;e++){var f=g[e];"string"===typeof f.filter&&(f.filter=f.filter.replaceAll("&","&"));if(Array.isArray(a)){if(-1!==a.indexOf(f.filter))return!0}else if("function"===
|
||||
typeof f.filter)if(f.filter.call(d,d.row(b).data(),b)){if("or"===c.combiner)return!0}else{if("and"===c.combiner)return!1}else if(a===f.filter||("string"!==typeof a||0!==a.length)&&a==f.filter||null===f.filter&&"string"===typeof a&&""===a)return!0}return"and"===c.combiner?!0:!1};k.prototype._searchContSetup=function(){this.c.controls&&this.s.colOpts.controls&&h(this.dom.searchButton).appendTo(this.dom.searchLabelCont);!1===this.c.dtOpts.searching||!1===this.s.colOpts.dtOpts.searching||null!==this.customPaneSettings&&
|
||||
void 0!==this.customPaneSettings.dtOpts&&void 0!==this.customPaneSettings.dtOpts.searching&&!this.customPaneSettings.dtOpts.searching||h(this.dom.searchLabelCont).appendTo(this.dom.searchCont)};k.prototype._searchExtras=function(){var a=this.s.updating;this.s.updating=!0;var b=this.s.dtPane.rows({selected:!0}).data().pluck("filter").toArray(),c=b.indexOf(!1!==this.s.colOpts.emptyMessage?this.s.colOpts.emptyMessage:this.c.emptyMessage),d=h(this.s.dtPane.table().container());-1<c&&(b[c]="");0<b.length?
|
||||
d.addClass(this.classes.selected):0===b.length&&d.removeClass(this.classes.selected);this.s.updating=a};k.prototype._uniqueRatio=function(a,b){return 0<b&&(0<this.s.rowData.totalOptions&&!this.s.dt.page.info().serverSide||this.s.dt.page.info().serverSide&&0<this.s.tableLength)?a/this.s.rowData.totalOptions:1};k.prototype._updateCommon=function(a){void 0===a&&(a=!1);if(!(this.s.dt.page.info().serverSide||void 0===this.s.dtPane||this.s.filteringActive&&!this.c.cascadePanes&&!0!==a||!0===this.c.cascadePanes&&
|
||||
!0===this.s.selectPresent||this.s.lastSelect&&this.s.lastCascade)){var b=this.s.colOpts,c=this.s.dtPane.rows({selected:!0}).data().toArray();a=h(this.s.dtPane.table().node()).parent()[0].scrollTop;var d=this.s.rowData;this.s.dtPane.clear();if(this.colExists){0===d.arrayFilter.length?this._populatePane():this.c.cascadePanes&&this.s.dt.rows().data().toArray().length===this.s.dt.rows({search:"applied"}).data().toArray().length?(d.arrayFilter=d.arrayOriginal,d.bins=d.binsOriginal):(this.c.viewTotal||
|
||||
this.c.cascadePanes)&&this._populatePane();this.c.viewTotal?this._detailsPane():d.binsTotal=d.bins;this.c.viewTotal&&!this.c.cascadePanes&&(d.arrayFilter=d.arrayTotals);for(var e=function(p){if(p&&(void 0!==d.bins[p.filter]&&0!==d.bins[p.filter]&&g.c.cascadePanes||!g.c.cascadePanes||g.s.clearing)){var n=g._addRow(p.display,p.filter,g.c.viewTotal?void 0!==d.bins[p.filter]?d.bins[p.filter]:0:d.bins[p.filter],g.c.viewTotal?String(d.binsTotal[p.filter]):d.bins[p.filter],p.sort,p.type),x=c.findIndex(function(z){return z.filter===
|
||||
p.filter});-1!==x&&(n.select(),c.splice(x,1))}},g=this,f=0,l=d.arrayFilter;f<l.length;f++)e(l[f])}if(void 0!==b.searchPanes&&void 0!==b.searchPanes.options||void 0!==b.options||null!==this.customPaneSettings&&void 0!==this.customPaneSettings.options)for(e=function(p){var n=c.findIndex(function(x){if(x.display===p.data().display)return!0});-1!==n&&(p.select(),c.splice(n,1))},f=0,l=this._getComparisonRows();f<l.length;f++)b=l[f],e(b);for(e=0;e<c.length;e++)b=c[e],b=this._addRow(b.display,b.filter,0,
|
||||
this.c.viewTotal?b.total:0,b.display,b.display),this.s.updating=!0,b.select(),this.s.updating=!1;this.s.dtPane.draw();this.s.dtPane.table().node().parentNode.scrollTop=a}};k.version="1.1.0";k.classes={buttonGroup:"dtsp-buttonGroup",buttonSub:"dtsp-buttonSub",clear:"dtsp-clear",clearAll:"dtsp-clearAll",clearButton:"clearButton",container:"dtsp-searchPane",countButton:"dtsp-countButton",disabledButton:"dtsp-disabledButton",dull:"dtsp-dull",hidden:"dtsp-hidden",hide:"dtsp-hide",layout:"dtsp-",name:"dtsp-name",
|
||||
nameButton:"dtsp-nameButton",nameCont:"dtsp-nameCont",narrow:"dtsp-narrow",paneButton:"dtsp-paneButton",paneInputButton:"dtsp-paneInputButton",pill:"dtsp-pill",search:"dtsp-search",searchCont:"dtsp-searchCont",searchIcon:"dtsp-searchIcon",searchLabelCont:"dtsp-searchButtonCont",selected:"dtsp-selected",smallGap:"dtsp-smallGap",subRow1:"dtsp-subRow1",subRow2:"dtsp-subRow2",subRowsContainer:"dtsp-subRowsContainer",title:"dtsp-title",topRow:"dtsp-topRow"};k.defaults={cascadePanes:!1,clear:!0,combiner:"or",
|
||||
controls:!0,container:function(a){return a.table().container()},dtOpts:{},emptyMessage:"<i>No Data</i>",hideCount:!1,layout:"columns-3",name:void 0,orderable:!0,orthogonal:{display:"display",filter:"filter",hideCount:!1,search:"filter",show:void 0,sort:"sort",threshold:.6,type:"type"},preSelect:[],threshold:.6,viewTotal:!1};return k}(),q,A,G=function(){function k(a,b,c){var d=this;void 0===c&&(c=!1);this.regenerating=!1;if(!A||!A.versionCheck||!A.versionCheck("1.10.0"))throw Error("SearchPane requires DataTables 1.10 or newer");
|
||||
if(!A.select)throw Error("SearchPane requires Select");var e=new A.Api(a);this.classes=q.extend(!0,{},k.classes);this.c=q.extend(!0,{},k.defaults,b);this.dom={clearAll:q('<button type="button">Clear All</button>').addClass(this.classes.clearAll),container:q("<div/>").addClass(this.classes.panes).text(e.i18n("searchPanes.loadMessage","Loading Search Panes...")),emptyMessage:q("<div/>").addClass(this.classes.emptyMessage),options:q("<div/>").addClass(this.classes.container),panes:q("<div/>").addClass(this.classes.container),
|
||||
title:q("<div/>").addClass(this.classes.title),titleRow:q("<div/>").addClass(this.classes.titleRow),wrapper:q("<div/>")};this.s={colOpts:[],dt:e,filterCount:0,filterPane:-1,page:0,panes:[],selectionList:[],serverData:{},stateRead:!1,updating:!1};if(void 0===e.settings()[0]._searchPanes){this._getState();if(this.s.dt.page.info().serverSide)e.on("preXhr.dt",function(g,f,l){void 0===l.searchPanes&&(l.searchPanes={});g=0;for(f=d.s.selectionList;g<f.length;g++){var p=f[g],n=d.s.dt.column(p.index).dataSrc();
|
||||
void 0===l.searchPanes[n]&&(l.searchPanes[n]={});for(var x=0;x<p.rows.length;x++)l.searchPanes[n][x]=p.rows[x].filter}});e.on("xhr",function(g,f,l,p){l&&l.searchPanes&&l.searchPanes.options&&(d.s.serverData=l,d.s.serverData.tableLength=l.recordsTotal,d._serverTotals())});e.settings()[0]._searchPanes=this;this.dom.clearAll.text(e.i18n("searchPanes.clearMessage","Clear All"));if(this.s.dt.settings()[0]._bInitComplete||c)this._paneDeclare(e,a,b);else e.one("preInit.dt",function(g){d._paneDeclare(e,a,
|
||||
b)});return this}}k.prototype.clearSelections=function(){this.dom.container.find(this.classes.search).each(function(){q(this).val("");q(this).trigger("input")});for(var a=[],b=0,c=this.s.panes;b<c.length;b++){var d=c[b];void 0!==d.s.dtPane&&a.push(d.clearPane())}this.s.dt.draw();return a};k.prototype.getNode=function(){return this.dom.container};k.prototype.rebuild=function(a,b){void 0===a&&(a=!1);void 0===b&&(b=!1);q(this.dom.emptyMessage).remove();var c=[];!1===a&&q(this.dom.panes).empty();for(var d=
|
||||
0,e=this.s.panes;d<e.length;d++){var g=e[d];if(!1===a||g.s.index===a)g.clearData(),c.push(g.rebuildPane(void 0!==this.s.selectionList[this.s.selectionList.length-1]?g.s.index===this.s.selectionList[this.s.selectionList.length-1].index:!1,this.s.dt.page.info().serverSide?this.s.serverData:void 0,null,b)),q(this.dom.panes).append(g.dom.container)}this.s.dt.page.info().serverSide||this.s.dt.draw();this.c.cascadePanes||this.c.viewTotal?this.redrawPanes(!0):this._updateSelection();this._updateFilterCount();
|
||||
this._attachPaneContainer();this.s.dt.draw();return 1===c.length?c[0]:c};k.prototype.redrawPanes=function(a){void 0===a&&(a=!1);var b=this.s.dt;if(!this.s.updating&&!this.s.dt.page.info().serverSide){var c=!0,d=this.s.filterPane;if(b.rows({search:"applied"}).data().toArray().length===b.rows().data().toArray().length)c=!1;else if(this.c.viewTotal)for(var e=0,g=this.s.panes;e<g.length;e++){var f=g[e];if(void 0!==f.s.dtPane){var l=f.s.dtPane.rows({selected:!0}).data().toArray().length;if(0===l)for(var p=
|
||||
0,n=this.s.selectionList;p<n.length;p++){var x=n[p];x.index===f.s.index&&0!==x.rows.length&&(l=x.rows.length)}0<l&&-1===d?d=f.s.index:0<l&&(d=null)}}g=void 0;e=[];if(this.regenerating){g=-1;1===e.length&&(g=e[0].index);a=0;for(e=this.s.panes;a<e.length;a++)if(f=e[a],void 0!==f.s.dtPane){b=!0;f.s.filteringActive=!0;if(-1!==d&&null!==d&&d===f.s.index||!1===c||f.s.index===g)b=!1,f.s.filteringActive=!1;f.updatePane(b?c:b)}this._updateFilterCount()}else{l=0;for(p=this.s.panes;l<p.length;l++)if(f=p[l],
|
||||
f.s.selectPresent){this.s.selectionList.push({index:f.s.index,rows:f.s.dtPane.rows({selected:!0}).data().toArray(),protect:!1});b.state.save();break}else f.s.deselect&&(g=f.s.index,n=f.s.dtPane.rows({selected:!0}).data().toArray(),0<n.length&&this.s.selectionList.push({index:f.s.index,rows:n,protect:!0}));if(0<this.s.selectionList.length)for(b=this.s.selectionList[this.s.selectionList.length-1].index,l=0,p=this.s.panes;l<p.length;l++)f=p[l],f.s.lastSelect=f.s.index===b;for(f=0;f<this.s.selectionList.length;f++)if(this.s.selectionList[f].index!==
|
||||
g||!0===this.s.selectionList[f].protect){b=!1;for(l=f+1;l<this.s.selectionList.length;l++)this.s.selectionList[l].index===this.s.selectionList[f].index&&(b=!0);b||(e.push(this.s.selectionList[f]),this.s.selectionList[f].protect=!1)}g=-1;1===e.length&&(g=e[0].index);l=0;for(p=this.s.panes;l<p.length;l++)if(f=p[l],void 0!==f.s.dtPane){b=!0;f.s.filteringActive=!0;if(-1!==d&&null!==d&&d===f.s.index||!1===c||f.s.index===g)b=!1,f.s.filteringActive=!1;f.updatePane(b?c:!1)}this._updateFilterCount();if(0<
|
||||
e.length&&(e.length<this.s.selectionList.length||a))for(this._cascadeRegen(e),b=e[e.length-1].index,d=0,a=this.s.panes;d<a.length;d++)f=a[d],f.s.lastSelect=f.s.index===b;else if(0<e.length)for(f=0,a=this.s.panes;f<a.length;f++)if(e=a[f],void 0!==e.s.dtPane){b=!0;e.s.filteringActive=!0;if(-1!==d&&null!==d&&d===e.s.index||!1===c)b=!1,e.s.filteringActive=!1;e.updatePane(b?c:b)}}c||(this.s.selectionList=[])}};k.prototype._attach=function(){var a=this;q(this.dom.container).removeClass(this.classes.hide);
|
||||
q(this.dom.titleRow).removeClass(this.classes.hide);q(this.dom.titleRow).remove();q(this.dom.title).appendTo(this.dom.titleRow);this.c.clear&&(q(this.dom.clearAll).appendTo(this.dom.titleRow),q(this.dom.clearAll).on("click.dtsps",function(){a.clearSelections()}));q(this.dom.titleRow).appendTo(this.dom.container);for(var b=0,c=this.s.panes;b<c.length;b++)q(c[b].dom.container).appendTo(this.dom.panes);q(this.dom.panes).appendTo(this.dom.container);0===q("div."+this.classes.container).length&&q(this.dom.container).prependTo(this.s.dt);
|
||||
return this.dom.container};k.prototype._attachExtras=function(){q(this.dom.container).removeClass(this.classes.hide);q(this.dom.titleRow).removeClass(this.classes.hide);q(this.dom.titleRow).remove();q(this.dom.title).appendTo(this.dom.titleRow);this.c.clear&&q(this.dom.clearAll).appendTo(this.dom.titleRow);q(this.dom.titleRow).appendTo(this.dom.container);return this.dom.container};k.prototype._attachMessage=function(){try{var a=this.s.dt.i18n("searchPanes.emptyPanes","No SearchPanes")}catch(b){a=
|
||||
null}if(null===a)q(this.dom.container).addClass(this.classes.hide),q(this.dom.titleRow).removeClass(this.classes.hide);else return q(this.dom.container).removeClass(this.classes.hide),q(this.dom.titleRow).addClass(this.classes.hide),q(this.dom.emptyMessage).text(a),this.dom.emptyMessage.appendTo(this.dom.container),this.dom.container};k.prototype._attachPaneContainer=function(){for(var a=0,b=this.s.panes;a<b.length;a++)if(!0===b[a].s.displayed)return this._attach();return this._attachMessage()};k.prototype._cascadeRegen=
|
||||
function(a){this.regenerating=!0;var b=-1;1===a.length&&(b=a[0].index);for(var c=0,d=this.s.panes;c<d.length;c++){var e=d[c];e.setCascadeRegen(!0);e.setClear(!0);(void 0!==e.s.dtPane&&e.s.index===b||void 0!==e.s.dtPane)&&e.clearPane();e.setClear(!1)}this._makeCascadeSelections(a);this.s.selectionList=a;a=0;for(b=this.s.panes;a<b.length;a++)e=b[a],e.setCascadeRegen(!1);this.regenerating=!1};k.prototype._checkMessage=function(){for(var a=0,b=this.s.panes;a<b.length;a++)if(!0===b[a].s.displayed)return;
|
||||
return this._attachMessage()};k.prototype._getState=function(){var a=this.s.dt.state.loaded();a&&a.searchPanes&&void 0!==a.searchPanes.selectionList&&(this.s.selectionList=a.searchPanes.selectionList)};k.prototype._makeCascadeSelections=function(a){for(var b=0;b<a.length;b++)for(var c=function(f){if(f.s.index===a[b].index&&void 0!==f.s.dtPane){b===a.length-1&&(f.s.lastCascade=!0);0<f.s.dtPane.rows({selected:!0}).data().toArray().length&&void 0!==f.s.dtPane&&(f.setClear(!0),f.clearPane(),f.setClear(!1));
|
||||
for(var l=function(x){f.s.dtPane.rows().every(function(z){void 0!==f.s.dtPane.row(z).data()&&void 0!==x&&f.s.dtPane.row(z).data().filter===x.filter&&f.s.dtPane.row(z).select()})},p=0,n=a[b].rows;p<n.length;p++)l(n[p]);d._updateFilterCount();f.s.lastCascade=!1}},d=this,e=0,g=this.s.panes;e<g.length;e++)c(g[e]);this.s.dt.state.save()};k.prototype._paneDeclare=function(a,b,c){var d=this;a.columns(0<this.c.columns.length?this.c.columns:void 0).eq(0).each(function(l){d.s.panes.push(new v(b,c,l,d.c.layout,
|
||||
d.dom.panes))});for(var e=a.columns().eq(0).toArray().length,g=this.c.panes.length,f=0;f<g;f++)this.s.panes.push(new v(b,c,e+f,this.c.layout,this.dom.panes,this.c.panes[f]));if(0<this.c.order.length)for(e=this.c.order.map(function(l,p,n){return d._findPane(l)}),this.dom.panes.empty(),this.s.panes=e,e=0,g=this.s.panes;e<g.length;e++)this.dom.panes.append(g[e].dom.container);this.s.dt.settings()[0]._bInitComplete?this._startup(a):this.s.dt.settings()[0].aoInitComplete.push({fn:function(){d._startup(a)}})};
|
||||
k.prototype._findPane=function(a){for(var b=0,c=this.s.panes;b<c.length;b++){var d=c[b];if(a===d.s.name)return d}};k.prototype._serverTotals=function(){for(var a=!1,b=!1,c=this.s.dt,d=0,e=this.s.panes;d<e.length;d++){var g=e[d];if(g.s.selectPresent){this.s.selectionList.push({index:g.s.index,rows:g.s.dtPane.rows({selected:!0}).data().toArray(),protect:!1});c.state.save();g.s.selectPresent=!1;a=!0;break}else g.s.deselect&&(b=g.s.dtPane.rows({selected:!0}).data().toArray(),0<b.length&&this.s.selectionList.push({index:g.s.index,
|
||||
rows:b,protect:!0}),b=a=!0)}if(a){c=[];for(d=0;d<this.s.selectionList.length;d++){g=!1;for(e=d+1;e<this.s.selectionList.length;e++)this.s.selectionList[e].index===this.s.selectionList[d].index&&(g=!0);if(!g){e=!1;a=0;for(var f=this.s.panes;a<f.length;a++)g=f[a],g.s.index===this.s.selectionList[d].index&&0<g.s.dtPane.rows({selected:!0}).data().toArray().length&&(e=!0);e&&c.push(this.s.selectionList[d])}}this.s.selectionList=c}else this.s.selectionList=[];c=-1;if(b&&1===this.s.selectionList.length)for(b=
|
||||
0,d=this.s.panes;b<d.length;b++)g=d[b],g.s.lastSelect=!1,g.s.deselect=!1,void 0!==g.s.dtPane&&0<g.s.dtPane.rows({selected:!0}).data().toArray().length&&(c=g.s.index);else if(0<this.s.selectionList.length)for(b=this.s.selectionList[this.s.selectionList.length-1].index,d=0,e=this.s.panes;d<e.length;d++)g=e[d],g.s.lastSelect=g.s.index===b,g.s.deselect=!1;else if(0===this.s.selectionList.length)for(b=0,d=this.s.panes;b<d.length;b++)g=d[b],g.s.lastSelect=!1,g.s.deselect=!1;q(this.dom.panes).empty();b=
|
||||
0;for(d=this.s.panes;b<d.length;b++)g=d[b],g.s.lastSelect?g._setListeners():g.rebuildPane(void 0,this.s.dt.page.info().serverSide?this.s.serverData:void 0,g.s.index===c?!0:null,!0),q(this.dom.panes).append(g.dom.container),void 0!==g.s.dtPane&&(q(g.s.dtPane.table().node()).parent()[0].scrollTop=g.s.scrollTop,q.fn.dataTable.select.init(g.s.dtPane));this.s.dt.page.info().serverSide||this.s.dt.draw()};k.prototype._startup=function(a){var b=this;q(this.dom.container).text("");this._attachExtras();q(this.dom.container).append(this.dom.panes);
|
||||
q(this.dom.panes).empty();var c=this.s.dt.state.loaded();if(this.c.viewTotal&&!this.c.cascadePanes&&null!==c&&void 0!==c&&void 0!==c.searchPanes&&void 0!==c.searchPanes.panes){for(var d=!1,e=0,g=c.searchPanes.panes;e<g.length;e++){var f=g[e];if(0<f.selected.length){d=!0;break}}if(d)for(d=0,e=this.s.panes;d<e.length;d++)f=e[d],f.s.showFiltered=!0}d=0;for(e=this.s.panes;d<e.length;d++)f=e[d],f.rebuildPane(void 0,0<Object.keys(this.s.serverData).length?this.s.serverData:void 0),q(this.dom.panes).append(f.dom.container);
|
||||
this.s.dt.page.info().serverSide||this.s.dt.draw();this.s.stateRead||null===c||void 0===c||(this.s.dt.page(c.start/this.s.dt.page.len()),this.s.dt.draw("page"));this.s.stateRead=!0;if(this.c.viewTotal&&!this.c.cascadePanes)for(c=0,d=this.s.panes;c<d.length;c++)f=d[c],f.updatePane();this._updateFilterCount();this._checkMessage();a.on("preDraw.dtsps",function(){b._updateFilterCount();!b.c.cascadePanes&&!b.c.viewTotal||b.s.dt.page.info().serverSide?b._updateSelection():b.redrawPanes();b.s.filterPane=
|
||||
-1});this.s.dt.on("stateSaveParams.dtsp",function(l,p,n){void 0===n.searchPanes&&(n.searchPanes={});n.searchPanes.selectionList=b.s.selectionList});if(this.s.dt.page.info().serverSide)a.off("page"),a.on("page",function(){b.s.page=b.s.dt.page()}),a.off("preXhr.dt"),a.on("preXhr.dt",function(l,p,n){void 0===n.searchPanes&&(n.searchPanes={});p=l=0;for(var x=b.s.panes;p<x.length;p++){var z=x[p],y=b.s.dt.column(z.s.index).dataSrc();void 0===n.searchPanes[y]&&(n.searchPanes[y]={});if(void 0!==z.s.dtPane){z=
|
||||
z.s.dtPane.rows({selected:!0}).data().toArray();for(var w=0;w<z.length;w++)n.searchPanes[y][w]=z[w].filter,l++}}b.c.viewTotal&&b._prepViewTotal();0<l&&(l!==b.s.filterCount?(n.start=0,b.s.page=0):n.start=b.s.page*b.s.dt.page.len(),b.s.dt.page(b.s.page),b.s.filterCount=l)});else a.on("preXhr.dt",function(l,p,n){l=0;for(p=b.s.panes;l<p.length;l++)p[l].clearData()});this.s.dt.on("xhr",function(l,p,n,x){var z=!1;if(!b.s.dt.page.info().serverSide)b.s.dt.one("preDraw",function(){if(!z){var y=b.s.dt.page();
|
||||
z=!0;q(b.dom.panes).empty();for(var w=0,u=b.s.panes;w<u.length;w++){var B=u[w];B.clearData();B.rebuildPane(void 0!==b.s.selectionList[b.s.selectionList.length-1]?B.s.index===b.s.selectionList[b.s.selectionList.length-1].index:!1,void 0,void 0,!0);q(b.dom.panes).append(B.dom.container)}b.s.dt.page.info().serverSide||b.s.dt.draw();b.c.cascadePanes||b.c.viewTotal?b.redrawPanes(b.c.cascadePanes):b._updateSelection();b._checkMessage();b.s.dt.one("draw",function(){b.s.dt.page(y).draw(!1)})}})});c=0;for(d=
|
||||
this.s.panes;c<d.length;c++)if(f=d[c],void 0!==f&&void 0!==f.s.dtPane&&(void 0!==f.s.colOpts.preSelect&&0<f.s.colOpts.preSelect.length||null!==f.customPaneSettings&&void 0!==f.customPaneSettings.preSelect&&0<f.customPaneSettings.preSelect.length)){e=f.s.dtPane.rows().data().toArray().length;for(g=0;g<e;g++)(-1!==f.s.colOpts.preSelect.indexOf(f.s.dtPane.cell(g,0).data())||null!==f.customPaneSettings&&void 0!==f.customPaneSettings.preSelect&&-1!==f.customPaneSettings.preSelect.indexOf(f.s.dtPane.cell(g,
|
||||
0).data()))&&f.s.dtPane.row(g).select();f.updateTable()}if(void 0!==this.s.selectionList&&0<this.s.selectionList.length)for(c=this.s.selectionList[this.s.selectionList.length-1].index,d=0,e=this.s.panes;d<e.length;d++)f=e[d],f.s.lastSelect=f.s.index===c;0<this.s.selectionList.length&&this.c.cascadePanes&&this._cascadeRegen(this.s.selectionList);this._updateFilterCount();a.on("destroy.dtsps",function(){for(var l=0,p=b.s.panes;l<p.length;l++)p[l].destroy();a.off(".dtsps");q(b.dom.clearAll).off(".dtsps");
|
||||
q(b.dom.container).remove();b.clearSelections()});if(this.c.clear)q(this.dom.clearAll).on("click.dtsps",function(){b.clearSelections()});a.settings()[0]._searchPanes=this};k.prototype._prepViewTotal=function(){for(var a=this.s.filterPane,b=!1,c=0,d=this.s.panes;c<d.length;c++){var e=d[c];if(void 0!==e.s.dtPane){var g=e.s.dtPane.rows({selected:!0}).data().toArray().length;0<g&&-1===a?(a=e.s.index,b=!0):0<g&&(a=null)}}c=0;for(d=this.s.panes;c<d.length;c++)if(e=d[c],void 0!==e.s.dtPane&&(e.s.filteringActive=
|
||||
!0,-1!==a&&null!==a&&a===e.s.index||!1===b))e.s.filteringActive=!1};k.prototype._updateFilterCount=function(){for(var a=0,b=0,c=this.s.panes;b<c.length;b++){var d=c[b];void 0!==d.s.dtPane&&(a+=d.getPaneCount())}b=this.s.dt.i18n("searchPanes.title","Filters Active - %d",a);q(this.dom.title).text(b);void 0!==this.c.filterChanged&&"function"===typeof this.c.filterChanged&&this.c.filterChanged.call(this.s.dt,a)};k.prototype._updateSelection=function(){this.s.selectionList=[];for(var a=0,b=this.s.panes;a<
|
||||
b.length;a++){var c=b[a];void 0!==c.s.dtPane&&this.s.selectionList.push({index:c.s.index,rows:c.s.dtPane.rows({selected:!0}).data().toArray(),protect:!1})}this.s.dt.state.save()};k.version="1.2.2";k.classes={clear:"dtsp-clear",clearAll:"dtsp-clearAll",container:"dtsp-searchPanes",emptyMessage:"dtsp-emptyMessage",hide:"dtsp-hidden",panes:"dtsp-panesContainer",search:"dtsp-search",title:"dtsp-title",titleRow:"dtsp-titleRow"};k.defaults={cascadePanes:!1,clear:!0,container:function(a){return a.table().container()},
|
||||
columns:[],filterChanged:void 0,layout:"columns-3",order:[],panes:[],viewTotal:!1};return k}();(function(k){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return k(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net")(a,b).$);return k(b,a,a.document)}:k(window.jQuery,window,document)})(function(k,a,b){function c(e,g){void 0===g&&(g=!1);e=new d.Api(e);var f=e.init().searchPanes||
|
||||
d.defaults.searchPanes;return(new G(e,f,g)).getNode()}m(k);t(k);var d=k.fn.dataTable;k.fn.dataTable.SearchPanes=G;k.fn.DataTable.SearchPanes=G;k.fn.dataTable.SearchPane=v;k.fn.DataTable.SearchPane=v;a=k.fn.dataTable.Api.register;a("searchPanes()",function(){return this});a("searchPanes.clearSelections()",function(){return this.iterator("table",function(e){e._searchPanes&&e._searchPanes.clearSelections()})});a("searchPanes.rebuildPane()",function(e,g){return this.iterator("table",function(f){f._searchPanes&&
|
||||
f._searchPanes.rebuild(e,g)})});a("searchPanes.container()",function(){var e=this.context[0];return e._searchPanes?e._searchPanes.getNode():null});k.fn.dataTable.ext.buttons.searchPanesClear={text:"Clear Panes",action:function(e,g,f,l){g.searchPanes.clearSelections()}};k.fn.dataTable.ext.buttons.searchPanes={action:function(e,g,f,l){e.stopPropagation();this.popover(l._panes.getNode(),{align:"dt-container"});l._panes.rebuild(void 0,!0)},config:{},init:function(e,g,f){var l=new k.fn.dataTable.SearchPanes(e,
|
||||
k.extend({filterChanged:function(n){e.button(g).text(e.i18n("searchPanes.collapse",{0:"SearchPanes",_:"SearchPanes (%d)"},n))}},f.config)),p=e.i18n("searchPanes.collapse","SearchPanes",0);e.button(g).text(p);f._panes=l},text:"Search Panes"};k(b).on("preInit.dt.dtsp",function(e,g,f){"dt"===e.namespace&&(g.oInit.searchPanes||d.defaults.searchPanes)&&(g._searchPanes||c(g,!0))});d.ext.feature.push({cFeature:"P",fnInit:c});d.ext.features&&d.ext.features.register("searchPanes",c)})})();
|
||||
|
||||
|
||||
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-searchpanes"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);console.log(b.fn.dataTable);b.fn.dataTable.SearchPanes||(console.log("not present"),require("datatables.net-searchpanes")(a,b));return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b){a=c.fn.dataTable;
|
||||
c.extend(!0,a.SearchPane.classes,{buttonGroup:"btn-group col justify-content-end",disabledButton:"disabled",dull:"",narrow:"col",pane:{container:"table"},paneButton:"btn btn-light",pill:"pill badge badge-pill badge-secondary",search:"col-sm form-control search",searchCont:"input-group col-sm",searchLabelCont:"input-group-append",subRow1:"dtsp-subRow1",subRow2:"dtsp-subRow2",table:"table table-sm table-borderless",topRow:"dtsp-topRow row"});c.extend(!0,a.SearchPanes.classes,{clearAll:"dtsp-clearAll col-auto btn btn-light",
|
||||
container:"dtsp-searchPanes",panes:"dtsp-panes dtsp-container",title:"dtsp-title col",titleRow:"dtsp-titleRow row"});return a.searchPanes});
|
||||
|
||||
|
7
app/frontend/static/assets/vendors/js/hammer.min.js
vendored
Normal file
5
app/frontend/static/assets/vendors/js/jquery.min.js
vendored
Normal file
@ -14,10 +14,10 @@
|
||||
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
|
||||
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
|
||||
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css">
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" />
|
||||
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/crafty.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/crafty-toggle-btn.css">
|
||||
|
||||
<link rel="manifest" href="/static/assets/crafty.webmanifest">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@ -32,7 +32,7 @@
|
||||
<!-- endinject -->
|
||||
|
||||
<!-- Plugin css for this page-->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||
<script src="../static/assets/vendors/js/jquery.min.js"></script>
|
||||
<!-- End Plugin css for this page-->
|
||||
|
||||
<!-- Layout styles -->
|
||||
@ -44,21 +44,15 @@
|
||||
<link rel="stylesheet" href="/static/assets/css/crafty.css">
|
||||
|
||||
<!-- Alpine.js - The modern jQuery alternative -->
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script defer src="../static/assets/vendors/js/cdn.min.js"></script>
|
||||
<!-- End Alpine.js -->
|
||||
|
||||
<!-- Bootstrap Toggle -->
|
||||
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
|
||||
<script defer src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
|
||||
integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"
|
||||
integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.1/chartjs-plugin-zoom.min.js"
|
||||
integrity="sha512-klQv6lz2YR+MecyFYMFRuU2eAl8IPRo6zHnsc9n142TJuJHS8CG0ix4Oq9na9ceeg1u5EkBfZsFcV3U7J51iew=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link href="../static/assets/vendors/css/bootstrap-toggle.min.css" rel="stylesheet">
|
||||
<script defer src="../static/assets/vendors/js/bootstrap-toggle.min.js"></script>
|
||||
<script src="../static/assets/vendors/js/chart.min.js"></script>
|
||||
<script src="../static/assets/vendors/js/hammer.min.js"></script>
|
||||
<script src="../static/assets/vendors/js/chartjs-plugin-zoom.min.js"></script>
|
||||
|
||||
<!-- End Bootstrap Toggle -->
|
||||
|
||||
@ -88,11 +82,13 @@
|
||||
<span class="mdi mdi-chevron-double-left"></span>
|
||||
<span class="mdi mdi-chevron-double-right"></span>
|
||||
</button>
|
||||
|
||||
<span class="badge-pill badge-outline-primary" id="server-name-nav" style="display: none;"></span>
|
||||
|
||||
|
||||
{% include notify.html %}
|
||||
|
||||
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button"
|
||||
data-toggle="offcanvas">
|
||||
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button" data-toggle="offcanvas">
|
||||
<span class="mdi mdi-menu"></span>
|
||||
</button>
|
||||
</div>
|
||||
@ -132,19 +128,32 @@
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
padding-left: 0.7rem;
|
||||
width: 180px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
margin-right: 1rem;
|
||||
background: var(--card-banner-bg);
|
||||
-webkit-transition: right 0.75s, opacity 0.75s, top 0.75s;
|
||||
-moz-transition: right 0.75s, opacity 0.75s, top 0.75s;
|
||||
-o-transition: right 0.75s, opacity 0.75s, top 0.75s;
|
||||
transition: right 0.75s, opacity 0.75s, top 0.75s;
|
||||
right: -6rem;
|
||||
right: -20rem;
|
||||
opacity: 0.1;
|
||||
margin-bottom: 1rem;
|
||||
z-index: 999;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
background-color: var(--card-banner-bg);
|
||||
color: var(--base-text);
|
||||
}
|
||||
|
||||
.toast-body {
|
||||
background-color: var(--dropdown-bg);
|
||||
color: var(--base-text);
|
||||
}
|
||||
|
||||
.notification img {
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.notification strong {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.notification.active {
|
||||
@ -158,34 +167,23 @@
|
||||
top: -2rem;
|
||||
}
|
||||
|
||||
.notification p {
|
||||
margin: 0px;
|
||||
width: calc(160.8px - 16px);
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
.notification span {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.46rem;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
font-size: 22px;
|
||||
font-size: 15px;
|
||||
user-select: none;
|
||||
z-index: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="notifications"></div>
|
||||
|
||||
|
||||
<div class="notifications"></div>
|
||||
|
||||
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
|
||||
<script src="/static/assets/js/shared/off-canvas.js"></script>
|
||||
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
|
||||
<script src="/static/assets/js/shared/misc.js"></script>
|
||||
<script type="text/javascript"
|
||||
src="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.0/bootbox.min.js"></script>
|
||||
<script type="text/javascript" src="../static/assets/vendors/js/datatables.min.js"></script>
|
||||
<script src="../static/assets/vendors/js/bootbox.min.js"></script>
|
||||
<script type="text/javascript" src="/static/assets/js/motd.js"></script>
|
||||
|
||||
<script>
|
||||
@ -241,6 +239,7 @@
|
||||
|
||||
let usingWebSockets = false;
|
||||
let webSocket = null;
|
||||
let websocketTimeoutId = null;
|
||||
// {% if request.protocol == 'https' %}
|
||||
usingWebSockets = true;
|
||||
|
||||
@ -291,18 +290,19 @@
|
||||
wsOpen = false;
|
||||
console.log('Closed WebSocket', closeEvent);
|
||||
|
||||
if (typeof reconnectorId !== 'number') {
|
||||
setTimeout(sendWssError, 7000);
|
||||
if (!document.hidden) {
|
||||
if (typeof reconnectorId !== 'number') {
|
||||
setTimeout(sendWssError, 7000);
|
||||
}
|
||||
console.info("Reconnecting with a timeout of", (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000, "milliseconds");
|
||||
// Discard old websocket and create a new one in 5 seconds
|
||||
wsInternal = null
|
||||
reconnectorId = setTimeout(startWebSocket, (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000)
|
||||
|
||||
failedConnectionCounter++;
|
||||
}
|
||||
console.info("Reconnecting with a timeout of", (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000, "milliseconds");
|
||||
// Discard old websocket and create a new one in 5 seconds
|
||||
wsInternal = null
|
||||
reconnectorId = setTimeout(startWebSocket, (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000)
|
||||
|
||||
failedConnectionCounter++;
|
||||
};
|
||||
|
||||
|
||||
webSocket = {
|
||||
on: function (event, callback) {
|
||||
console.log('registered ' + event + ' event');
|
||||
@ -315,6 +315,12 @@
|
||||
}
|
||||
|
||||
wsInternal.send(JSON.stringify(message));
|
||||
},
|
||||
close: function (code, reason) {
|
||||
wsInternal.close(code, reason);
|
||||
},
|
||||
getStatus: function () {
|
||||
return wsInternal.readyState;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -328,6 +334,21 @@
|
||||
warn('WebSockets are not supported in Crafty if not using the https protocol')
|
||||
// {% end%}
|
||||
|
||||
// Managing Connexions for Multi Tab opened to reduce bandwith usage
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState == "hidden") {
|
||||
websocketTimeoutId = setTimeout(() => {
|
||||
webSocket.close(1000, "Closed due to Inactivity");
|
||||
console.log('%c[Crafty Controller] %cClose Websocket due to Tab not active after 5s', 'font-weight: 900; color: #800080;', 'font-weight: 900; color: #eee;');
|
||||
}, 10000);
|
||||
} else {
|
||||
clearTimeout(websocketTimeoutId)
|
||||
if (webSocket.getStatus() == WebSocket.CLOSED) {
|
||||
startWebSocket();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (webSocket) {
|
||||
webSocket.on('send_start_error', function (start_error) {
|
||||
var x = document.querySelector('.bootbox');
|
||||
@ -363,18 +384,21 @@
|
||||
if (x) {
|
||||
x.remove()
|
||||
}
|
||||
bootbox.alert({
|
||||
bootbox.confirm({
|
||||
title: "{{ translate('notify', 'downloadLogs', data['lang']) }}",
|
||||
message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}",
|
||||
buttons: {
|
||||
ok: {
|
||||
confirm: {
|
||||
label: 'Download',
|
||||
className: 'btn-info'
|
||||
}
|
||||
},
|
||||
callback: function () {
|
||||
console.log("in callback")
|
||||
location.href = "/panel/download_support_package";
|
||||
callback: function (result) {
|
||||
if (result){
|
||||
location.href = "/panel/download_support_package";
|
||||
} else {
|
||||
bootbox.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -488,30 +512,55 @@
|
||||
|
||||
function notify(message) {
|
||||
console.log(`notify(${message})`);
|
||||
var paragraphEl = document.createElement('p');
|
||||
var closeEl = document.createElement('span');
|
||||
var intId = getRandomInt(0, 100);
|
||||
var toastDiv = document.createElement('div');
|
||||
toastDiv.setAttribute("id", "toast_" + intId);
|
||||
toastDiv.setAttribute("class", "notification toast");
|
||||
toastDiv.setAttribute("role", "alert");
|
||||
toastDiv.setAttribute("aria-lived", "assertive");
|
||||
toastDiv.setAttribute("aria-atomic", "true");
|
||||
toastDiv.setAttribute("data-delay", "3000");
|
||||
toastDiv.setAttribute("data-animation", "true");
|
||||
toastDiv.setAttribute("data-autohide", "false");
|
||||
|
||||
paragraphEl.textContent = message;
|
||||
var toastHeaderDiv = document.createElement('div');
|
||||
toastHeaderDiv.setAttribute("class", "toast-header");
|
||||
|
||||
closeEl.innerHTML = '×';
|
||||
closeEl.addEventListener('click', function () { closeNotification(this) });
|
||||
var toastHeaderImg = document.createElement('img');
|
||||
toastHeaderImg.setAttribute("src", "/static/assets/images/logo_small.svg");
|
||||
toastHeaderImg.setAttribute("class", "mr-auto");
|
||||
toastHeaderImg.setAttribute("alt", "logo");
|
||||
toastHeaderDiv.appendChild(toastHeaderImg);
|
||||
|
||||
var parentEl = document.createElement('div');
|
||||
parentEl.appendChild(paragraphEl);
|
||||
parentEl.appendChild(closeEl);
|
||||
var toastHeaderTitle = document.createElement('strong');
|
||||
toastHeaderTitle.setAttribute("class", "mr-auto");
|
||||
toastHeaderTitle.innerHTML = " Crafty Controller ";
|
||||
toastHeaderDiv.appendChild(toastHeaderTitle);
|
||||
|
||||
parentEl.classList.add('notification');
|
||||
var toastHeaderCloseSpan = document.createElement('span');
|
||||
toastHeaderCloseSpan.setAttribute("class", "fa-solid fa-xmark");
|
||||
toastHeaderCloseSpan.setAttribute("aria-hidden", "true");
|
||||
toastHeaderCloseSpan.addEventListener('click', function () { closeNotification(this.parentElement) });
|
||||
toastHeaderDiv.appendChild(toastHeaderCloseSpan);
|
||||
|
||||
document.querySelector('.notifications').appendChild(parentEl);
|
||||
var toastBodyDiv = document.createElement('div');
|
||||
toastBodyDiv.setAttribute("class", "toast-body");
|
||||
toastBodyDiv.textContent = message;
|
||||
|
||||
toastDiv.appendChild(toastHeaderDiv);
|
||||
toastDiv.appendChild(toastBodyDiv);
|
||||
|
||||
document.querySelector('.notifications').appendChild(toastDiv);
|
||||
|
||||
$('#toast_' + intId).toast('show');
|
||||
|
||||
setTimeout(function () {
|
||||
parentEl.classList.add('active');
|
||||
toastDiv.classList.add('active');
|
||||
}, 200);
|
||||
|
||||
setTimeout(function (element) {
|
||||
closeNotification(element);
|
||||
}, 7500, closeEl);
|
||||
closeNotification(element.parentElement);
|
||||
}, 7500, toastHeaderCloseSpan);
|
||||
|
||||
`
|
||||
<div class="notification">
|
||||
@ -524,10 +573,10 @@
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
console.log('%c[Crafty Controller] %cAlpine.js pre-initialization!', 'font-weight: 900; color: #800080;', 'color: #eee;');
|
||||
})
|
||||
});
|
||||
document.addEventListener('alpine:initialized', () => {
|
||||
console.log('%c[Crafty Controller] %cAlpine.js initialized!', 'font-weight: 900; color: #800080;', 'color: #eee;');
|
||||
})
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
console.log('%c[Crafty Controller] %cReady for JS!', 'font-weight: 900; color: #800080;', 'font-weight: 900; color: #eee;');
|
||||
@ -559,4 +608,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" data-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<li class="nav-item dropdown notif-li">
|
||||
<a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" aria-expanded="false">
|
||||
<i class="fas fa-broadcast-tower
|
||||
{% if data.get('update_available') %}
|
||||
text-danger
|
||||
@ -21,12 +20,10 @@
|
||||
|
||||
<li class="nav-item dropdown user-dropdown">
|
||||
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
|
||||
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
|
||||
alt="Profile image"> </a>
|
||||
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a>
|
||||
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
|
||||
<div class="dropdown-header text-center">
|
||||
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
|
||||
alt="Profile image">
|
||||
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image">
|
||||
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
|
||||
<p class="font-weight-light text-muted mb-0">Roles: </p>
|
||||
{% for r in data['user_role'] %}
|
||||
@ -38,23 +35,19 @@
|
||||
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
|
||||
</div>
|
||||
{% if data['user_data']['preparing'] %}
|
||||
<span class="dropdown-item" id="support_progress"><i
|
||||
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
|
||||
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
|
||||
data['lang']) }}<br><br></span>
|
||||
<span class="dropdown-item" id="support_progress">
|
||||
<div class="support_progress" style="height: 15px; width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar"
|
||||
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
</span>
|
||||
{% else %}
|
||||
<a class="dropdown-item" id="support_logs"><i
|
||||
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
|
||||
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
|
||||
data['lang']) }}</i></a>
|
||||
{% end %}
|
||||
{% if data['superuser'] %}
|
||||
<a class="dropdown-item" href="/panel/activity_logs"><i
|
||||
class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
|
||||
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
|
||||
'activityLog', data['lang']) }}</a>
|
||||
{% end %}
|
||||
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{
|
||||
@ -65,6 +58,7 @@
|
||||
<style>
|
||||
.badge-notify {
|
||||
background: var(--purple);
|
||||
color: var(--dark);
|
||||
position: absolute;
|
||||
-moz-transform: translate(-70%, -70%);
|
||||
/* For Firefox */
|
||||
@ -78,16 +72,19 @@
|
||||
.clear-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.notif-div::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.notif-div {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.notif-div::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.notif-div {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function pfpError(image) {
|
||||
@ -96,21 +93,24 @@
|
||||
return true;
|
||||
}
|
||||
function updateAnnouncements(data) {
|
||||
console.log(data)
|
||||
console.log(data);
|
||||
let text = "";
|
||||
for (let value of data) {
|
||||
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
|
||||
}
|
||||
if (data.length > 0) {
|
||||
localStorage.setItem("notif-count", data.length);
|
||||
$("#notif-count").show()
|
||||
$("#notif-count").html(data.length);
|
||||
$("#announcements").html(text);
|
||||
} else {
|
||||
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
|
||||
</p>`);
|
||||
$("#notif-count").hide()
|
||||
}
|
||||
$(".clear-button").on("click", function (event) {
|
||||
console.log("CLEAR BUTTON")
|
||||
event.stopPropagation();
|
||||
let uuid = event.target.getAttribute("data-id");
|
||||
$(`#${uuid}`).remove();
|
||||
send_clear(uuid);
|
||||
@ -136,10 +136,14 @@
|
||||
});
|
||||
let responseData = await res.json();
|
||||
console.log(responseData);
|
||||
setTimeout(function() {
|
||||
getAnnouncements();
|
||||
}, 1800000); //Wait 30 minutes in miliseconds
|
||||
console.log("Registered annoucement fetch event in 30 minutes.")
|
||||
if (responseData.status === "ok") {
|
||||
updateAnnouncements(responseData.data)
|
||||
} else {
|
||||
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>")
|
||||
updateAnnouncements([])
|
||||
}
|
||||
}
|
||||
async function send_clear(uuid) {
|
||||
@ -161,6 +165,28 @@
|
||||
bootbox.alert(responseData.error)
|
||||
}
|
||||
}
|
||||
|
||||
/* Open / Close with Button */
|
||||
$('li.dropdown.notif-li a').on('click', function (event) {
|
||||
$(this).parent().toggleClass("show");
|
||||
$('div.notif-div').toggleClass("show");
|
||||
if ($('li.dropdown.notif-li a').attr('aria-expanded') === 'false') {
|
||||
$('li.dropdown.notif-li a').attr('aria-expanded', "true");
|
||||
}
|
||||
else {
|
||||
$('li.dropdown.notif-li a').attr('aria-expanded', "false");
|
||||
}
|
||||
});
|
||||
|
||||
/* Close when clicking ouside */
|
||||
$('body').on('click', function (e) {
|
||||
if (!$('li.dropdown.notif-li').is(e.target) && $('li.dropdown.notif-li').has(e.target).length === 0 && $('show').has(e.target).length === 0) {
|
||||
$('li.notif-li').removeClass("show");
|
||||
$('li.dropdown.notif-li a').attr('aria-expanded', "false");
|
||||
$('div.notif-div').removeClass("show");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
getAnnouncements();
|
||||
})
|
||||
|
@ -170,7 +170,7 @@
|
||||
{% block js %}
|
||||
<script>
|
||||
function replacer(key, value) {
|
||||
if (key == "disabled_language_files") {
|
||||
if (key == "disabled_language_files" || key == "monitored_mounts") {
|
||||
if (value == 0) {
|
||||
return []
|
||||
} else {
|
||||
@ -336,6 +336,6 @@
|
||||
});
|
||||
})
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js">
|
||||
<script src="../../static/assets/vendors/js/bootstrap-select.min.js">
|
||||
</script>
|
||||
{% end %}
|
@ -58,13 +58,11 @@
|
||||
|
||||
</div>
|
||||
<div class="wrapper my-auto ml-auto ml-lg-4">
|
||||
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true"
|
||||
title="{% raw translate('dashboard', 'cpuCores', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cores') }} <br /> {% raw translate('dashboard', 'cpuCurFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> {% raw translate('dashboard', 'cpuMaxFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_max_freq') }}">
|
||||
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true" title="{% raw translate('dashboard', 'cpuCores', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cores') }} <br /> {% raw translate('dashboard', 'cpuCurFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> {% raw translate('dashboard', 'cpuMaxFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_max_freq') }}">
|
||||
{{ translate('dashboard', 'cpuUsage', data['lang']) }}: <span id="cpu_usage">{{
|
||||
data.get('hosts_data').get('cpu_usage') }}</span>
|
||||
</p>
|
||||
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top"
|
||||
title="{{ translate('dashboard', 'memUsage', data['lang']) }}: {{ data.get('hosts_data').get('mem_usage') }}">
|
||||
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top" title="{{ translate('dashboard', 'memUsage', data['lang']) }}: {{ data.get('hosts_data').get('mem_usage') }}">
|
||||
{{ translate('dashboard', 'memUsage', data['lang']) }}: <span id="mem_percent">{{
|
||||
data.get('hosts_data').get('mem_percent') }}%</span>
|
||||
</p>
|
||||
@ -104,19 +102,19 @@
|
||||
<div class="col-12 mt-4">
|
||||
<div class="d-flex">
|
||||
<div class="wrapper" style="width: 100%;">
|
||||
<h5 class="mb-1 font-weight-medium text-primary">Storage
|
||||
{% if len(data["monitored"]) > 0 %}
|
||||
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'storage',
|
||||
data['lang']) }}
|
||||
</h5>
|
||||
{% end %}
|
||||
<div id="storage_data">
|
||||
<div class="row">
|
||||
{% for item in data['hosts_data']['disk_json'] %}
|
||||
{% if item["mount"] in data["monitored"] %}
|
||||
<div id="{{item['device']}}" class="col-xl-3 col-lg-3 col-md-4 col-12">
|
||||
<h4 class="mb-0 font-weight-semibold d-inline-block text-truncate storage-heading"
|
||||
id="title_{{item['device']}}" data-toggle="tooltip" data-placement="bottom"
|
||||
title="{{item['mount']}}" style="max-width: 100%;"><i class="fas fa-hdd"></i>
|
||||
<h4 class="mb-0 font-weight-semibold d-inline-block text-truncate storage-heading" id="title_{{item['device']}}" data-toggle="tooltip" data-placement="bottom" title="{{item['mount']}}" style="max-width: 100%;"><i class="fas fa-hdd"></i>
|
||||
{{item["mount"]}}</h4>
|
||||
<div class="progress d-inline-block"
|
||||
style="height: 20px; width: 100%; background-color: rgb(139, 139, 139) !important;">
|
||||
<div class="progress d-inline-block" style="height: 20px; width: 100%; background-color: rgb(139, 139, 139) !important;">
|
||||
<div class="progress-bar
|
||||
{% if item['percent_used'] <= 58 %}
|
||||
bg-success
|
||||
@ -125,8 +123,7 @@
|
||||
{% else %}
|
||||
bg-danger
|
||||
{% end %}
|
||||
" role="progressbar" style="color: black; height: 100%; width: {{item['percent_used']}}%;"
|
||||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">{{item["used"]}} /
|
||||
" role="progressbar" style="color: black; height: 100%; width: {{item['percent_used']}}%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">{{item["used"]}} /
|
||||
{{item["total"]}}
|
||||
</div>
|
||||
</div>
|
||||
@ -153,9 +150,7 @@
|
||||
data['lang']) }}</h4>
|
||||
{% if len(data['servers']) > 0 %}
|
||||
{% if data['user_data']['hints'] %}
|
||||
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}" ,
|
||||
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
|
||||
data-placement="top"></span>
|
||||
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}" , data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" , data-placement="top"></span>
|
||||
{% end %}
|
||||
{% end %}
|
||||
<div><a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> {{
|
||||
@ -193,8 +188,7 @@
|
||||
<td draggable="false">
|
||||
<i class="fas fa-server"></i>
|
||||
{% if server['alert'] %}
|
||||
<a style="color: red !important;" draggable="false"
|
||||
href="/panel/server_detail?id={{server['server_data']['server_id']}}">
|
||||
<a style="color: red !important;" draggable="false" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
|
||||
{{ server['server_data']['server_name'] }} <i class="fas fa-exclamation-triangle"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
@ -208,13 +202,11 @@
|
||||
{% if server['user_command_permission'] %}
|
||||
{% if server['stats']['importing'] and server['stats']['running'] %}
|
||||
<!-- WHAT HAPPENED HERE -->
|
||||
<a data-id="{{server['server_data']['server_id']}}" class=""><i
|
||||
class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'installing',
|
||||
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'installing',
|
||||
data['lang']) }}</i></a>
|
||||
{% elif server['stats']['updating']%}
|
||||
<!-- WHAT HAPPENED HERE -->
|
||||
<a data-id="{{server['server_data']['server_id']}}" class=""><i
|
||||
class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'updating',
|
||||
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'updating',
|
||||
data['lang']) }}</i></a>
|
||||
{% elif server['stats']['waiting_start']%}
|
||||
<!-- WHAT HAPPENED HERE -->
|
||||
@ -226,31 +218,25 @@
|
||||
{{ translate('serverTerm', 'importing',
|
||||
data['lang']) }}</a>
|
||||
{% elif server['stats']['running'] %}
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="stop_button" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'stop' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="stop_button" data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
|
||||
<i class="fas fa-stop"></i>
|
||||
</a>
|
||||
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="restart_button" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'restart' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="restart_button" data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
|
||||
<i class="fas fa-sync"></i>
|
||||
</a>
|
||||
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<i class="fas fa-skull"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="play_button" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'start' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="play_button" data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
|
||||
<i class="fas fa-play"></i>
|
||||
</a>
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="clone_button" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'clone' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="clone_button" data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
|
||||
<i class="fas fa-clone"></i>
|
||||
</a>
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<i class="fas fa-skull"></i>
|
||||
</a>
|
||||
{% end %}
|
||||
@ -258,8 +244,7 @@
|
||||
</td>
|
||||
|
||||
<td draggable="false" id="server_cpu_{{server['server_data']['server_id']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
|
||||
title="{{server['stats']['cpu']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['cpu']}}">
|
||||
<div class="progress-bar
|
||||
{% if server['stats']['cpu'] <= 33 %}
|
||||
bg-success
|
||||
@ -268,15 +253,13 @@
|
||||
{% else %}
|
||||
bg-danger
|
||||
{% end %}
|
||||
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{{server['stats']['cpu']}}%
|
||||
</td>
|
||||
|
||||
<td draggable="false" id="server_mem_{{server['server_data']['server_id']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
|
||||
title="{{server['stats']['mem']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['mem']}}">
|
||||
<div class="progress-bar
|
||||
{% if server['stats']['mem_percent'] <= 33 %}
|
||||
bg-success
|
||||
@ -285,8 +268,7 @@
|
||||
{% else %}
|
||||
bg-danger
|
||||
{% end %}
|
||||
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{{server['stats']['mem_percent']}}% -
|
||||
|
||||
@ -305,8 +287,7 @@
|
||||
data['lang']) }} <br />
|
||||
|
||||
{% if server['stats']['desc'] != 'False' %}
|
||||
<div id="desc_id"
|
||||
style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">{{
|
||||
<div id="desc_id" style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">{{
|
||||
server['stats']['desc'] }}</div> <br />
|
||||
{% end %}
|
||||
|
||||
@ -334,16 +315,14 @@
|
||||
<br />
|
||||
<br />
|
||||
</td>
|
||||
<span class="server-player-totals" id="server_players_{{server['server_data']['server_id']}}"
|
||||
data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span>
|
||||
<span class="server-player-totals" id="server_players_{{server['server_data']['server_id']}}" data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span>
|
||||
</tr>
|
||||
{% end %}
|
||||
</div>
|
||||
</span>
|
||||
{% for server in data['failed_servers'] %}
|
||||
<tr id="{{server['server_id']}}" draggable="false">
|
||||
<td class="text-warning"><i class="fas fa-server"></i> <a class="text-warning"
|
||||
href="/panel/server_detail?id={{server['server_id']}}&subpage=config">{{server['server_name']}}</a>
|
||||
<td class="text-warning"><i class="fas fa-server"></i> <a class="text-warning" href="/panel/server_detail?id={{server['server_id']}}&subpage=config">{{server['server_name']}}</a>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
@ -368,28 +347,22 @@
|
||||
<div class="row">
|
||||
<div class="col-10 col-lg-3 mx-0 px-0">
|
||||
{% if server['alert'] %}
|
||||
<a style="color: red !important" class="btn btn-link d-flex justify-content-start" type="button"
|
||||
href="/panel/server_detail?id={{server['server_data']['server_id']}}">
|
||||
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }} <i
|
||||
class="fas fa-exclamation-triangle"></i>
|
||||
<a style="color: red !important" class="btn btn-link d-flex justify-content-start" type="button" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
|
||||
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }} <i class="fas fa-exclamation-triangle"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="btn btn-link d-flex justify-content-start" type="button"
|
||||
href="/panel/server_detail?id={{server['server_data']['server_id']}}">
|
||||
<a class="btn btn-link d-flex justify-content-start" type="button" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
|
||||
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }}
|
||||
</a>
|
||||
{% end %}
|
||||
</div>
|
||||
<div class="col-2 col-lg-3 mx-0 px-0">
|
||||
<a class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse"
|
||||
data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false"
|
||||
aria-controls="collapse-{{server['server_data']['server_id']}}">
|
||||
<a class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse" data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false" aria-controls="collapse-{{server['server_data']['server_id']}}">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 col-lg-3 mx-0 px-0">
|
||||
<a id="m_server_running_status_{{server['server_data']['server_id']}}"
|
||||
class="btn btn-link d-flex justify-content-start" type="button">
|
||||
<a id="m_server_running_status_{{server['server_data']['server_id']}}" class="btn btn-link d-flex justify-content-start" type="button">
|
||||
{% if server['stats']['running'] %}
|
||||
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online',
|
||||
data['lang']) }}</span>
|
||||
@ -410,23 +383,17 @@
|
||||
{% if server['stats']['running'] %}
|
||||
<div class="row">
|
||||
<div class="col-4 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}"
|
||||
class="btn btn-link stop_button actions_serveritem" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'stop' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link stop_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
|
||||
<i class="fas fa-stop"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}"
|
||||
class="btn btn-link restart_button actions_serveritem" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'restart' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link restart_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
|
||||
<i class="fas fa-sync"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}"
|
||||
class="btn btn-link kill_button actions_serveritem" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link kill_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<i class="fas fa-skull"></i>
|
||||
</a>
|
||||
</div>
|
||||
@ -452,31 +419,24 @@
|
||||
{% elif server['stats']['importing']%}
|
||||
<div class="row">
|
||||
<div class="col-12 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link"><i
|
||||
class="fa fa-spinner fa-spin"></i>
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link"><i class="fa fa-spinner fa-spin"></i>
|
||||
{{ translate('serverTerm', 'importing', data['lang']) }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-4 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}"
|
||||
class="btn play_button actions_serveritem" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'start' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn play_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
|
||||
<i class="fas fa-play"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}"
|
||||
class="btn clone_button actions_serveritem" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'clone' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn clone_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
|
||||
<i class="fas fa-clone"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 px-0">
|
||||
<a data-id="{{server['server_data']['server_id']}}"
|
||||
class="btn kill_button actions_serveritem" data-toggle="tooltip"
|
||||
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<a data-id="{{server['server_data']['server_id']}}" class="btn kill_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
|
||||
<i class="fas fa-skull"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
@ -488,15 +448,13 @@
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse"
|
||||
aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
|
||||
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse" aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h6>{{ translate('dashboard', 'cpuUsage', data['lang']) }}</h6>
|
||||
<div id="m_server_cpu_{{server['server_data']['server_id']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
|
||||
title="{{server['stats']['cpu']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['cpu']}}">
|
||||
<div class="progress-bar
|
||||
{% if server['stats']['cpu'] <= 33 %}
|
||||
bg-success
|
||||
@ -505,8 +463,7 @@
|
||||
{% else %}
|
||||
bg-danger
|
||||
{% end %}
|
||||
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{{server['stats']['cpu']}}%
|
||||
</div>
|
||||
@ -514,8 +471,7 @@
|
||||
<div class="col-6">
|
||||
<h6>{{ translate('dashboard', 'memUsage', data['lang']) }}</h6>
|
||||
<div draggable="false" id="m_server_mem_{{server['server_data']['server_id']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
|
||||
title="{{server['stats']['mem']}}">
|
||||
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['mem']}}">
|
||||
<div class="progress-bar
|
||||
{% if server['stats']['mem_percent'] <= 33 %}
|
||||
bg-success
|
||||
@ -524,8 +480,7 @@
|
||||
{% else %}
|
||||
bg-danger
|
||||
{% end %}
|
||||
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{{server['stats']['mem_percent']}}% -
|
||||
|
||||
@ -554,8 +509,7 @@
|
||||
data['lang']) }} <br />
|
||||
|
||||
{% if server['stats']['desc'] != 'False' %}
|
||||
<div id="desc_id"
|
||||
style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">
|
||||
<div id="desc_id" style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">
|
||||
{{ server['stats']['desc'] }}</div> <br />
|
||||
{% end %}
|
||||
|
||||
@ -664,7 +618,7 @@
|
||||
}
|
||||
|
||||
|
||||
function warn(message, link = null, className = null) {
|
||||
function warnServer(message, link = null, className = null) {
|
||||
var closeEl = document.createElement('span');
|
||||
var strongEL = document.createElement('strong');
|
||||
var msgEl = document.createElement('div');
|
||||
@ -796,6 +750,7 @@
|
||||
/* Update Motd */
|
||||
let motd = "";
|
||||
if (server.desc) {
|
||||
m_motd = `<span id="m_input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`;
|
||||
motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`;
|
||||
m_server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; overflow: auto;">' + motd + '</div>' + "<br />";
|
||||
server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; max-width: 85px !important; overflow: auto;">' + motd + '</div>' + "<br />";
|
||||
@ -847,7 +802,7 @@
|
||||
setTimeout(finishTimeout, 60000);
|
||||
});
|
||||
function finishTimeout() {
|
||||
warn("It seems this is taking a while...it's possible you're using UBlock or a similar ad blocker and it's causing some of our connections to not make it to the server. Try disabling your ad blocker.",
|
||||
warnServer("It seems this is taking a while...it's possible you're using UBlock or a similar ad blocker and it's causing some of our connections to not make it to the server. Try disabling your ad blocker.",
|
||||
null, 'wssError');
|
||||
}
|
||||
$(".stop_button").click(function () {
|
||||
@ -1040,23 +995,28 @@
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
function sendOrder(id_string) {
|
||||
async function sendOrder(id_string) {
|
||||
const token = getCookie("_xsrf")
|
||||
|
||||
$.ajax({
|
||||
type: "PATCH",
|
||||
headers: { 'X-XSRFToken': token },
|
||||
url: `/api/v2/users/@me`,
|
||||
data: JSON.stringify({
|
||||
let res = await fetch(`/api/v2/users/@me`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
server_order: id_string,
|
||||
}),
|
||||
success: function (data) {
|
||||
console.log("got response:");
|
||||
console.log(data);
|
||||
},
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
return
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
// Inits the sortable
|
||||
$("table#servers_table tbody")
|
||||
.sortable({
|
||||
|
73
app/frontend/templates/panel/loading.html
Normal file
@ -0,0 +1,73 @@
|
||||
{% extends ../base.html %}
|
||||
|
||||
{% block meta %}
|
||||
{% end %}
|
||||
|
||||
{% block title %}Crafty Controller Starting{% end %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-wrapper">
|
||||
<div class="card-header justify-content-between align-items-center" style="border: none;">
|
||||
<div id="image-div" style="width: 100%;">
|
||||
<img class="img-center" id="logo-animate" src="../static/assets/images/crafty-logo-square-1024.png"
|
||||
alt="Crafty Logo, Crafty is loading" width="20%" style="clear: both;">
|
||||
</div>
|
||||
<br>
|
||||
</br>
|
||||
<div id="text-div" style="width: 100%; text-align: center;">
|
||||
<h2 id="status" style="display: block;" data-init="{{ translate('startup', 'serverInit', data['lang']) }}"
|
||||
data-server="{{ translate('startup', 'server', data['lang']) }}"
|
||||
data-internet="{{ translate('startup', 'internet', data['lang']) }}"
|
||||
data-tasks="{{ translate('startup', 'tasks', data['lang']) }}"
|
||||
data-internals="{{ translate('startup', 'internals', data['lang']) }}"
|
||||
data-almost="{{ translate('startup', 'almost', data['lang']) }}">
|
||||
{{ translate('startup', 'starting', data['lang']) }}</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.img-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function rotateImage(degree) {
|
||||
$('#logo-animate').animate({ transform: degree }, {
|
||||
step: function (now, fx) {
|
||||
$(this).css({
|
||||
'-webkit-transform': 'rotate(' + now + 'deg)',
|
||||
'-moz-transform': 'rotate(' + now + 'deg)',
|
||||
'transform': 'rotate(' + now + 'deg)'
|
||||
});
|
||||
}
|
||||
});
|
||||
setTimeout(function () {
|
||||
rotateImage(360);
|
||||
}, 2000);
|
||||
}
|
||||
$(document).ready(function () {
|
||||
setTimeout(function () {
|
||||
rotateImage(360);
|
||||
}, 2000);
|
||||
if (webSocket) {
|
||||
webSocket.on('update', function (data) {
|
||||
if ("server" in data) {
|
||||
$("#status").html(`${$("#status").data(data.section)} ${data.server}...`);
|
||||
} else {
|
||||
$("#status").html($("#status").data(data.section));
|
||||
}
|
||||
});
|
||||
webSocket.on('send_start_reload', function () {
|
||||
setTimeout(function () {
|
||||
location.href = '/panel/dashboard'
|
||||
}, 5000);
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% end %}
|
@ -79,7 +79,7 @@
|
||||
<tbody>
|
||||
{% for user in data['users'] %}
|
||||
<tr>
|
||||
<td><i class="fas fa-user"></i> {{ user.username }}</td>
|
||||
<td><i class="fas fa-user"></i><span id="user_{{user.user_id}}">{{ user.username }}</span></td>
|
||||
<td>
|
||||
{% if user.enabled %}
|
||||
<span class="text-success">
|
||||
@ -106,7 +106,10 @@
|
||||
{% end %}
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
|
||||
<td><span data-translate="{{translate('userConfig', 'userName', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'userName', data['lang'])}}" id="username_{{user.user_id}}" class="edit_user clickable" data-name="{{user.username}}" data-id="{{user.user_id}}"><i class="fa-solid fa-user"></i></span>
|
||||
<span data-translate1="{{translate('userConfig', 'password', data['lang'])}}" data-translate2="{{translate('userConfig', 'repeat', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'password', data['lang'])}}" class="edit_password clickable" data-id="{{user.user_id}}"><i class="fa-solid fa-lock"></i></span>
|
||||
<a data-toggle="tooltip" title="{{ translate('userConfig', 'pageTitle', data['lang'])}}" href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
{% for user in data['managed_users'] %}
|
||||
@ -138,7 +141,10 @@
|
||||
{% end %}
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
|
||||
<td><span data-translate="{{translate('userConfig', 'userName', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'userName', data['lang'])}}" id="username_{{user.user_id}}" class="edit_user clickable" data-name="{{user.username}}" data-id="{{user.user_id}}"><i class="fa-solid fa-user"></i></span>
|
||||
<span data-translate1="{{translate('userConfig', 'password', data['lang'])}}" data-translate2="{{translate('userConfig', 'repeat', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'password', data['lang'])}}" class="edit_password clickable" data-id="{{user.user_id}}"><i class="fa-solid fa-lock"></i></span>
|
||||
<a data-toggle="tooltip" title="{{ translate('userConfig', 'pageTitle', data['lang'])}}" href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
</tbody>
|
||||
@ -274,6 +280,12 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.clickable {
|
||||
color: #007bff;
|
||||
}
|
||||
.clickable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-picker {
|
||||
border: 1px solid var(--outline);
|
||||
}
|
||||
@ -312,6 +324,99 @@
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
function validateForm() {
|
||||
let password0 = document.getElementById("password0").value;
|
||||
let password1 = document.getElementById("password1").value;
|
||||
if (password0 != password1) {
|
||||
$('.passwords-match').popover('show');
|
||||
$('.popover-body').click(function () {
|
||||
$('.passwords-match').popover("hide");
|
||||
});
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
$("#password0").css("outline", "1px solid red");
|
||||
$("#password1").css("outline", "1px solid red");
|
||||
return false;
|
||||
} else {
|
||||
return password1;
|
||||
}
|
||||
}
|
||||
$(".edit_password").on("click", async function(){
|
||||
const token = getCookie("_xsrf");
|
||||
let user_id = $(this).data('id');
|
||||
bootbox.confirm(`<form class="form" id='infos' action=''>\
|
||||
<div class="form-group">
|
||||
<label for="new_password">${$(this).data("translate1")}</label>
|
||||
<input class="form-control" type='password' id="password0" name='new_password' /></br>\
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">${$(this).data("translate2")}</label>
|
||||
<input class="form-control" type='password' id="password1" name='confirm_password' />\
|
||||
</div>
|
||||
</form>`, async function(result) {
|
||||
if(result){
|
||||
password = validateForm();
|
||||
if (!password){
|
||||
return;
|
||||
}
|
||||
let res = await fetch(`/api/v2/users/${user_id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
body: JSON.stringify({"password": password}),
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
console.log(responseData.data)
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
$(document).on("submit", ".bootbox form", function(e) {
|
||||
e.preventDefault();
|
||||
$(".bootbox .btn-primary").click();
|
||||
});
|
||||
|
||||
$(".edit_user").on("click", function(){
|
||||
const token = getCookie("_xsrf");
|
||||
let username = $(this).data('name');
|
||||
let user_id = $(this).data('id');
|
||||
bootbox.confirm(`<form class="form" id='infos' action=''>\
|
||||
<div class="form-group">
|
||||
<label for="username">${$(this).data("translate")}</label>
|
||||
<input class="form-control" type='text' name='username' id="username_field" value=${username} /><br/>\
|
||||
</div>
|
||||
</form>`, async function(result) {
|
||||
if(result){
|
||||
let new_username = $("#username_field").val();
|
||||
let res = await fetch(`/api/v2/users/${user_id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
body: JSON.stringify({"username": new_username}),
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
$(`#user_${user_id}`).html(` ${new_username}`)
|
||||
$(`#username_${user_id}`).data('name', new_username);
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (webSocket) {
|
||||
webSocket.on('move_status', function (message) {
|
||||
if (message === "done") {
|
||||
|
@ -71,6 +71,7 @@ data['lang']) }}{% end %}
|
||||
data['lang']) }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if data['new_user'] %}
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang'])
|
||||
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang'])
|
||||
@ -98,6 +99,15 @@ data['lang']) }}{% end %}
|
||||
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
|
||||
data-placement="right"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang'])
|
||||
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang'])
|
||||
}}</small> </label>
|
||||
<input type="text" class="form-control" name="username" id="username" autocomplete="off"
|
||||
data-lpignore="true" value="{{ data['user']['username'] }}" placeholder="User Name" disabled>
|
||||
</div>
|
||||
{% end %}
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">{{ translate('userConfig', 'gravEmail', data['lang'])
|
||||
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'gravDesc', data['lang'])
|
||||
@ -353,6 +363,7 @@ data['lang']) }}{% end %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
const userId = new URLSearchParams(document.location.search).get('id');
|
||||
function submit_user(event) {
|
||||
if (!validateForm()) {
|
||||
event.preventDefault();
|
||||
@ -388,18 +399,41 @@ data['lang']) }}{% end %}
|
||||
return (isNaN(value) ? value : +value);
|
||||
}
|
||||
}
|
||||
const userId = new URLSearchParams(document.location.search).get('id')
|
||||
$("#user_form").on("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
let password = validateForm();
|
||||
if (!password){
|
||||
return;
|
||||
let password = null;
|
||||
if(!userId){
|
||||
password = validateForm();
|
||||
if (!password){
|
||||
return;
|
||||
}
|
||||
}
|
||||
const token = getCookie("_xsrf")
|
||||
|
||||
let userRes = await fetch(`/api/v2/users/@me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
});
|
||||
let userData = await userRes.json();
|
||||
let superuser = null;
|
||||
if (userData.status === "ok") {
|
||||
superuser = userData.data["superuser"];
|
||||
edit_id = userData.data["user_id"];
|
||||
} else {
|
||||
bootbox.alert({
|
||||
title: userData.error,
|
||||
message: userData.error
|
||||
});
|
||||
}
|
||||
|
||||
let userForm = document.getElementById("user_form");
|
||||
|
||||
let disabled_flag = false;
|
||||
let roles = $('.role_check').map(function() {
|
||||
let roles = null;
|
||||
if (superuser || userId != edit_id){
|
||||
roles = $('.role_check').map(function() {
|
||||
if ($(this).attr("disabled")){
|
||||
disabled_flag = true;
|
||||
}
|
||||
@ -407,7 +441,6 @@ data['lang']) }}{% end %}
|
||||
return $(this).val();
|
||||
}
|
||||
}).get();
|
||||
|
||||
let avail_permissions = $('.perm-name').map(function() {
|
||||
return $(this).data("perm");
|
||||
}).get();
|
||||
@ -416,20 +449,26 @@ data['lang']) }}{% end %}
|
||||
for(i=0; i < avail_permissions.length; i++){
|
||||
permissions.push({"name": avail_permissions[i], "quantity": $(`#quantity_${avail_permissions[i]}`).val(), "enabled": $(`#permission_${avail_permissions[i]}`).is(':checked')})
|
||||
}
|
||||
console.log(permissions);
|
||||
|
||||
}
|
||||
let formData = new FormData(userForm);
|
||||
//Create an object from the form data entries
|
||||
let formDataObject = Object.fromEntries(formData.entries());
|
||||
if(userId){
|
||||
delete formDataObject.username
|
||||
}
|
||||
if (superuser || userId != edit_id){
|
||||
if (!disabled_flag){
|
||||
formDataObject.roles = roles;
|
||||
}
|
||||
if ($("#permissions").length){
|
||||
formDataObject.permissions = permissions;
|
||||
}
|
||||
if(typeof password === "string"){
|
||||
if(!userId){
|
||||
if(typeof password === "string"){
|
||||
formDataObject.password = password;
|
||||
}
|
||||
}
|
||||
}
|
||||
formDataObject.enabled = $("#enabled").is(":checked");
|
||||
if ($("#superuser").is(":enabled")){
|
||||
formDataObject.superuser = $("#superuser").is(":checked");
|
||||
|
@ -248,12 +248,38 @@
|
||||
$("#player-body").html(text);
|
||||
|
||||
}
|
||||
|
||||
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||
function getCookie(name) {
|
||||
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||
return r ? r[1] : undefined;
|
||||
}
|
||||
const token = getCookie("_xsrf")
|
||||
$(window).ready(function () {
|
||||
console.log("ready!");
|
||||
|
||||
//if (webSocket) {
|
||||
webSocket.on('update_server_details', update_server_details);
|
||||
add_server_name();
|
||||
//}
|
||||
});
|
||||
async function add_server_name(){
|
||||
let res = await fetch(`/api/v2/servers/${serverId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
console.log(responseData)
|
||||
$("#server-name-nav").html(`${responseData.data['server_name']}`)
|
||||
$("#server-name-nav").show();
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.error,
|
||||
message: responseData.error_data
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
@ -31,6 +31,9 @@
|
||||
<a class="dropdown-item {% if data['active_link'] == 'admin_controls' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=admin_controls" role="tab" aria-selected="true"><i class="fas fa-users"></i> {{ translate('serverDetails', 'playerControls', data['lang']) }}</a>
|
||||
{% end %}
|
||||
<a class="dropdown-item {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true"><i class="fa-solid fa-chart-line"></i> {{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
||||
{% if data['permissions']['Config'] in data['user_permissions'] %}
|
||||
<a class="dropdown-item {% if data['active_link'] == 'webhooks' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks" role="tab" aria-selected="true"><i class="fa-regular fa-bell"></i>{{ translate('webhooks', 'webhooks', data['lang']) }}</a>
|
||||
{% end %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -53,4 +53,10 @@
|
||||
<a class="nav-link {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true">
|
||||
<i class="fa-solid fa-chart-line"></i>{{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
||||
</li>
|
||||
{% if data['permissions']['Config'] in data['user_permissions'] %}
|
||||
<li class="nav-item term-nav-item">
|
||||
<a class="nav-link {% if data['active_link'] == 'webhooks' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks" role="tab" aria-selected="true">
|
||||
<i class="fa-regular fa-bell"></i>{{ translate('webhooks', 'webhooks', data['lang']) }}</a>
|
||||
</li>
|
||||
{% end %}
|
||||
</ul>
|
@ -69,7 +69,7 @@
|
||||
<td>Banned on {{ player['banned_on'] }}</td>
|
||||
<td>Banned by : {{ player['source'] }} <br />Reason : {{ player['reason'] }}</td>
|
||||
<td class="buttons">
|
||||
<button onclick="send_command_to_server(`pardon {{ player['name'] }} `)" type="button" class="btn btn-danger">Unban</button>
|
||||
<button onclick="send_command_to_server(`pardon {{ player['name'] }}`)" type="button" class="btn btn-danger">Unban</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
|
@ -44,25 +44,25 @@
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<br>
|
||||
<br>
|
||||
<form id="backup-form" class="forms-sample">
|
||||
{% if data['backing_up'] %}
|
||||
<div class="progress" style="height: 15px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
|
||||
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
|
||||
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
|
||||
data['backup_stats']['percent'] }}%</div>
|
||||
</div>
|
||||
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
|
||||
id="total_files">{{data['server_stats']['world_size']}}</span></p>
|
||||
{% end %}
|
||||
{% if data['backing_up'] %}
|
||||
<div class="progress" style="height: 15px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
|
||||
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
|
||||
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
|
||||
data['backup_stats']['percent'] }}%</div>
|
||||
</div>
|
||||
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
|
||||
id="total_files">{{data['server_stats']['world_size']}}</span></p>
|
||||
{% end %}
|
||||
|
||||
<br>
|
||||
{% if not data['backing_up'] %}
|
||||
<div id="backup_button" class="form-group">
|
||||
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
|
||||
data['lang']) }}</button>
|
||||
</div>
|
||||
{% end %}
|
||||
<br>
|
||||
{% if not data['backing_up'] %}
|
||||
<div id="backup_button" class="form-group">
|
||||
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
|
||||
data['lang']) }}</button>
|
||||
</div>
|
||||
{% end %}
|
||||
<form id="backup-form" class="forms-sample">
|
||||
<div class="form-group">
|
||||
{% if data['super_user'] %}
|
||||
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
|
||||
@ -309,11 +309,6 @@
|
||||
|
||||
async function backup_started() {
|
||||
const token = getCookie("_xsrf")
|
||||
document.getElementById('backup_button').style.visibility = 'hidden';
|
||||
var dialog = bootbox.dialog({
|
||||
message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}",
|
||||
closeButton: false
|
||||
});
|
||||
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -323,8 +318,14 @@
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
console.log(responseData);
|
||||
process_tree_response(responseData);
|
||||
|
||||
$("#backup_button").html(`<div class="progress" style="height: 15px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
|
||||
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
|
||||
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
|
||||
data['backup_stats']['percent'] }}%</div>
|
||||
</div>
|
||||
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
|
||||
id="total_files">{{data['server_stats']['world_size']}}</span></p>`);
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
@ -427,6 +428,7 @@
|
||||
if ($("#root_files_button").hasClass("clicked")){
|
||||
formDataObject.exclusions = excluded;
|
||||
}
|
||||
delete formDataObject.root_path
|
||||
console.log(excluded);
|
||||
console.log(formDataObject);
|
||||
// Format the plain form data as JSON
|
||||
@ -649,10 +651,10 @@
|
||||
let checked = ""
|
||||
let dpath = value.path;
|
||||
let filename = key;
|
||||
if (value.excluded){
|
||||
checked = "checked"
|
||||
}
|
||||
if (value.dir){
|
||||
if (value.excluded){
|
||||
checked = "checked"
|
||||
}
|
||||
text += `<li class="tree-item" data-path="${dpath}">
|
||||
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
|
||||
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>
|
||||
|
@ -136,7 +136,7 @@
|
||||
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
|
||||
</small> </label>
|
||||
<input type="number" class="form-control" name="server_port" id="server_port" value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1" required>
|
||||
<span data-html="true" class="port-hint text-center" title="<i class='fal fa-exclamation-triangle'></i> " , data-content="{{
|
||||
<span data-html="true" class="port-hint text-center" title="<i class='fa-solid fa-triangle-exclamation'></i> " , data-content="{{
|
||||
translate('serverConfig', 'statsHint1' , data['lang'])}} <br> <br> <strong>{{ translate('serverConfig', 'statsHint2', data['lang'])}}</strong>" , data-placement="right"></span>
|
||||
</div>
|
||||
{% end %}
|
||||
|
@ -156,6 +156,9 @@
|
||||
right: 35px;
|
||||
}
|
||||
}
|
||||
.tree-file:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<ul class="tree-view">
|
||||
<li>
|
||||
|
@ -89,11 +89,11 @@
|
||||
const cpu = []
|
||||
{% for item in data['history_stats'] %}
|
||||
{% if 'minecraft-java' in data['server_stats']['server_type'] %}
|
||||
players.push("{{ item.online }}");
|
||||
players.push("{{ item.get('online') }}");
|
||||
{% end %}
|
||||
dates.push("{{ item.created.strftime('%Y/%m/%d, %H:%M:%S') }}");
|
||||
ram.push("{{ item.mem_percent }}")
|
||||
cpu.push("{{ item.cpu }}")
|
||||
dates.push("{{ item.get('created').strftime('%Y/%m/%d, %H:%M:%S') }}");
|
||||
ram.push("{{ item.get('mem_percent') }}")
|
||||
cpu.push("{{ item.get('cpu') }}")
|
||||
{% end %}
|
||||
var hist_chart = new Chart(ctxL, {
|
||||
type: 'line',
|
||||
|
@ -47,198 +47,184 @@
|
||||
<h4 class="card-title"><i class="fas fa-calendar"></i> {{ translate('serverSchedules',
|
||||
'scheduledTasks', data['lang']) }} </h4>
|
||||
{% if data['user_data']['hints'] %}
|
||||
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" ,
|
||||
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
|
||||
data-placement="bottom"></span>
|
||||
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" , data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" , data-placement="bottom"></span>
|
||||
{% end %}
|
||||
<div>
|
||||
<button
|
||||
onclick="location.href=`/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}`"
|
||||
class="btn btn-info">{{ translate('serverSchedules', 'create', data['lang']) }}<i
|
||||
class="fas fa-pencil-alt"></i></button>
|
||||
</div>
|
||||
<div><a class="btn btn-info" href="/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}"><i class="fas fa-plus-circle"></i> {{ translate('serverSchedules', 'create', data['lang']) }}</a></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table"
|
||||
style="table-layout:fixed;" aria-describedby="Schedule List">
|
||||
<thead>
|
||||
<tr class="rounded">
|
||||
<th style="width: 2%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }}
|
||||
</th>
|
||||
<th style="width: 23%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
|
||||
}}</th>
|
||||
<th style="width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'interval',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'nextRun',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'enabled',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'edit', data['lang'])
|
||||
}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for schedule in data['schedules'] %}
|
||||
<tr>
|
||||
<td id="{{schedule.schedule_id}}" class="id">
|
||||
<p>{{schedule.name}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.action}}" class="action">
|
||||
<p>{{schedule.action}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
|
||||
<p style="overflow: scroll;" class="no-scroll">{{schedule.command}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.interval}}" class="action">
|
||||
{% if schedule.interval_type != '' and schedule.interval_type != 'reaction' %}
|
||||
<p>{{ translate('serverSchedules', 'every', data['lang']) }}</p>
|
||||
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
|
||||
{% elif schedule.interval_type == 'reaction' %}
|
||||
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child', data['lang'])}}:
|
||||
{{ schedule.parent }}</p>
|
||||
{% else %}
|
||||
<p>Cron String:</p>
|
||||
<p>{{schedule.cron_string}}</p>
|
||||
{% end %}
|
||||
</td>
|
||||
<td id="{{schedule.start_time}}" class="action">
|
||||
<p>{{schedule.next_run}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.enabled}}" class="action">
|
||||
<input style="width: 10px !important;" type="checkbox" class="schedule-enabled-toggle"
|
||||
data-schedule-id="{{schedule.schedule_id}}"
|
||||
data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
|
||||
</td>
|
||||
<td id="{{schedule.action}}" class="action">
|
||||
<button
|
||||
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
|
||||
class="btn btn-info">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<br>
|
||||
<br>
|
||||
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table"
|
||||
style="table-layout:fixed;" aria-describedby="Schedule List Mobile">
|
||||
<thead>
|
||||
<tr class="rounded">
|
||||
<th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
|
||||
}}</th>
|
||||
<th style="max-width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'enabled',
|
||||
data['lang']) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for schedule in data['schedules'] %}
|
||||
<tr data-toggle="modal" data-target="#task_details_{{schedule.schedule_id}}">
|
||||
<td id="{{schedule.action}}" class="action">
|
||||
<p>{{schedule.action}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
|
||||
<p style="overflow: scroll;">{{schedule.command}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.enabled}}" class="action">
|
||||
{% if schedule.enabled %}
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-square"></i> {{ translate('serverSchedules', 'yes', data['lang']) }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-danger">
|
||||
<i class="far fa-times-square"></i> {{ translate('serverSchedules', 'no', data['lang']) }}
|
||||
</span>
|
||||
{% end %}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog"
|
||||
aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">{{ translate('serverSchedules', 'details',
|
||||
data['lang']) }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul style="list-style: none;">
|
||||
<li id="{{schedule.schedule_id}}" class="id" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'name', data['lang']) }}</h4>
|
||||
<p>{{schedule.schedule_id}}</p>
|
||||
</li>
|
||||
<li id="{{schedule.action}}" class="action" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'action', data['lang']) }}</h4>
|
||||
<p>{{schedule.action}}</p>
|
||||
</li>
|
||||
<li id="{{schedule.command}}" class="action" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'command', data['lang']) }}</h4>
|
||||
<p>{{schedule.command}}</p>
|
||||
</li>
|
||||
<li id="{{schedule.interval}}" class="action" style="border-top: .1em solid gray;">
|
||||
{% if schedule.interval != '' %}
|
||||
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
|
||||
<p>{{ translate('serverSchedules', 'every', data['lang']) }} {{schedule.interval}}
|
||||
{{schedule.interval_type}}</p>
|
||||
{% elif schedule.interval_type == 'reaction' %}
|
||||
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
|
||||
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child',
|
||||
data['lang']) }}: {{ schedule.parent }}</p>
|
||||
{% else %}
|
||||
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
|
||||
<p>{{ translate('serverSchedules', 'cron', data['lang']) }}: {{schedule.cron_string}}
|
||||
</p>
|
||||
{% end %}
|
||||
</li>
|
||||
<li id="{{schedule.start_time}}" class="action" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'nextRun', data['lang']) }}</h4>
|
||||
{% if schedule.next_run %}
|
||||
<p>{{schedule.next_run}}</p>
|
||||
{% else %}
|
||||
<p>zzzzzzz</p>
|
||||
{% end %}
|
||||
</li>
|
||||
<li id="{{schedule.enabled}}" class="action"
|
||||
style="border-top: .1em solid gray; border-bottom: .1em solid gray">
|
||||
<h4>{{ translate('serverSchedules', 'enabled', data['lang']) }}</h4>
|
||||
<input type="checkbox" class="schedule-enabled-toggle"
|
||||
data-schedule-id="{{schedule.schedule_id}}"
|
||||
data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
|
||||
class="btn btn-info">
|
||||
<i class="fas fa-pencil-alt"></i> {{ translate('serverSchedules', 'edit', data['lang'])
|
||||
}}
|
||||
</button>
|
||||
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i> {{ translate('serverSchedules',
|
||||
'delete', data['lang']) }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{
|
||||
translate('serverSchedules', 'close', data['lang']) }}</button>
|
||||
{% if len(data['schedules']) == 0 %}
|
||||
<div style="text-align: center; color: grey;">
|
||||
<h7>{{ translate('serverSchedules', 'no-schedule', data['lang']) }} <strong>{{ translate('serverSchedules', 'newSchedule',data['lang']) }}</strong>.</h7>
|
||||
</div>
|
||||
{% end %}
|
||||
{% if len(data['schedules']) > 0 %}
|
||||
<div class="d-none d-lg-block">
|
||||
<table class="table table-hover table-responsive" id="schedule_table" style="table-layout:fixed;" aria-describedby="Schedule List">
|
||||
<thead>
|
||||
<tr class="rounded">
|
||||
<th style="width: 5%; min-width: 64px;">{{ translate('serverSchedules', 'enabled',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 7%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }}
|
||||
</th>
|
||||
<th style="width: 23%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
|
||||
}}</th>
|
||||
<th style="width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'interval',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'nextRun',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'edit', data['lang'])
|
||||
}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for schedule in data['schedules'] %}
|
||||
<tr>
|
||||
<td id="{{schedule.enabled}}" class="enabled">
|
||||
<button type="button" class="btn btn-sm btn-info btn-toggle schedule-custom-toggle" id="schedule{{schedule.id}}" name="schedule{{schedule.id}}" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
|
||||
<div class="handle"></div>
|
||||
</button>
|
||||
</td>
|
||||
<td id="{{schedule.schedule_id}}" class="id">
|
||||
<p>{{schedule.name}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.action}}">
|
||||
<p>{{schedule.action}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.command}}" style="overflow: scroll; max-width: 30px;">
|
||||
<p style="overflow: scroll;" class="no-scroll">{{schedule.command}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.interval}}">
|
||||
{% if schedule.interval_type != '' and schedule.interval_type != 'reaction' %}
|
||||
<p>{{ translate('serverSchedules', 'every', data['lang']) }}</p>
|
||||
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
|
||||
{% elif schedule.interval_type == 'reaction' %}
|
||||
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child', data['lang'])}}:
|
||||
{{ schedule.parent }}</p>
|
||||
{% else %}
|
||||
<p>Cron String:</p>
|
||||
<p>{{schedule.cron_string}}</p>
|
||||
{% end %}
|
||||
</td>
|
||||
<td id="{{schedule.start_time}}">
|
||||
<p>{{schedule.next_run}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.action}}" class="action">
|
||||
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
</div>
|
||||
<div class=" d-block d-lg-none">
|
||||
<table class="table table-hover" id="mini_schedule_table" style="table-layout:fixed;" aria-describedby="Schedule List Mobile">
|
||||
<thead>
|
||||
<tr class="rounded">
|
||||
<th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
|
||||
}}</th>
|
||||
<th style="max-width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'enabled',
|
||||
data['lang']) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for schedule in data['schedules'] %}
|
||||
<tr data-toggle="modal" data-target="#task_details_{{schedule.schedule_id}}">
|
||||
<td id="{{schedule.action}}" class="action">
|
||||
<p>{{schedule.action}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
|
||||
<p style="overflow: scroll;">{{schedule.command}}</p>
|
||||
</td>
|
||||
<td id="{{schedule.enabled}}" class="enabled">
|
||||
<button type="button" class="btn btn-sm btn-info btn-toggle schedule-custom-toggle" id="schedule{{schedule.id}}" name="schedule{{schedule.id}}" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
|
||||
<div class="handle"></div>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">{{ translate('serverSchedules', 'details',
|
||||
data['lang']) }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul style="list-style: none;">
|
||||
<li id="{{schedule.schedule_id}}" class="id" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'name', data['lang']) }}</h4>
|
||||
<p>{{schedule.schedule_id}}</p>
|
||||
</li>
|
||||
<li id="{{schedule.action}}" class="action" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'action', data['lang']) }}</h4>
|
||||
<p>{{schedule.action}}</p>
|
||||
</li>
|
||||
<li id="{{schedule.command}}" class="action" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'command', data['lang']) }}</h4>
|
||||
<p>{{schedule.command}}</p>
|
||||
</li>
|
||||
<li id="{{schedule.interval}}" class="action" style="border-top: .1em solid gray;">
|
||||
{% if schedule.interval != '' %}
|
||||
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
|
||||
<p>{{ translate('serverSchedules', 'every', data['lang']) }} {{schedule.interval}}
|
||||
{{schedule.interval_type}}</p>
|
||||
{% elif schedule.interval_type == 'reaction' %}
|
||||
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
|
||||
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child',
|
||||
data['lang']) }}: {{ schedule.parent }}</p>
|
||||
{% else %}
|
||||
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
|
||||
<p>{{ translate('serverSchedules', 'cron', data['lang']) }}: {{schedule.cron_string}}
|
||||
</p>
|
||||
{% end %}
|
||||
</li>
|
||||
<li id="{{schedule.start_time}}" class="action" style="border-top: .1em solid gray;">
|
||||
<h4>{{ translate('serverSchedules', 'nextRun', data['lang']) }}</h4>
|
||||
{% if schedule.next_run %}
|
||||
<p>{{schedule.next_run}}</p>
|
||||
{% else %}
|
||||
<p>zzzzzzz</p>
|
||||
{% end %}
|
||||
</li>
|
||||
<li id="{{schedule.enabled}}" class="action" style="border-top: .1em solid gray; border-bottom: .1em solid gray">
|
||||
<h4>{{ translate('serverSchedules', 'enabled', data['lang']) }}</h4>
|
||||
<input type="checkbox" class="schedule-enabled-toggle" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
|
||||
<i class="fas fa-pencil-alt"></i> {{ translate('serverSchedules', 'edit', data['lang'])
|
||||
}}
|
||||
</button>
|
||||
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i> {{ translate('serverSchedules',
|
||||
'delete', data['lang']) }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{
|
||||
translate('serverSchedules', 'close', data['lang']) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -265,6 +251,16 @@
|
||||
.toggle {
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
td.enabled {
|
||||
vertical-align: middle;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
|
||||
.enabled .toggle-group .btn {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -299,20 +295,21 @@
|
||||
};
|
||||
}
|
||||
|
||||
$(() => {
|
||||
$('.schedule-enabled-toggle').bootstrapToggle({
|
||||
on: 'Yes',
|
||||
off: 'No',
|
||||
onstyle: 'success',
|
||||
offstyle: 'danger',
|
||||
})
|
||||
$('.schedule-enabled-toggle').each(function () {
|
||||
$(document).ready(function () {
|
||||
$('.schedule-custom-toggle').each(function () {
|
||||
const enabled = JSON.parse(this.getAttribute('data-schedule-enabled'));
|
||||
$(this).bootstrapToggle(enabled ? 'on' : 'off')
|
||||
if (enabled) {
|
||||
this.classList.add("active");
|
||||
}
|
||||
else {
|
||||
this.classList.remove("active");
|
||||
}
|
||||
})
|
||||
$('.schedule-enabled-toggle').change(function () {
|
||||
});
|
||||
$(() => {
|
||||
$('.schedule-custom-toggle').click(function () {
|
||||
const id = this.getAttribute('data-schedule-id');
|
||||
const enabled = this.checked;
|
||||
const enabled = !JSON.parse(this.getAttribute('data-schedule-enabled'));
|
||||
|
||||
fetch(`/api/v2/servers/{{data['server_id']}}/tasks/${id}`, {
|
||||
method: 'PATCH',
|
||||
@ -426,4 +423,4 @@
|
||||
|
||||
</script>
|
||||
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
280
app/frontend/templates/panel/server_webhook_edit.html
Normal file
@ -0,0 +1,280 @@
|
||||
{% extends ../base.html %}
|
||||
|
||||
{% block meta %}
|
||||
{% end %}
|
||||
|
||||
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Page Title Header Starts-->
|
||||
<div class="row page-title-header">
|
||||
<div class="col-12">
|
||||
<div class="page-header">
|
||||
<h4 class="page-title">
|
||||
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
|
||||
data['server_stats']['server_id']['server_name'] }}
|
||||
<br />
|
||||
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Page Title Header Ends-->
|
||||
|
||||
{% include "parts/details_stats.html" %}
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12 grid-margin">
|
||||
<div class="card">
|
||||
<div class="card-body pt-0">
|
||||
{% include "parts/server_controls_list.html" %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-8">
|
||||
{% if data['new_webhook'] == True %}
|
||||
<form class="forms-sample" method="post" id="new_webhook_form"
|
||||
action="/panel/new_webhook?id={{ data['server_stats']['server_id']['server_id'] }}">
|
||||
{% else %}
|
||||
<form class="forms-sample" method="post" id="webhook_form"
|
||||
action="/panel/edit_webhook?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['webhook']['id'] }}">
|
||||
{% end %}
|
||||
<select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type">
|
||||
{% if data['new_webhook'] == False %}
|
||||
<option value="{{data['webhook']['webhook_type']}}">{{data['webhook']['webhook_type']}}</option>
|
||||
{% end %}
|
||||
{% for type in data['providers'] %}
|
||||
{% if type != data['webhook']['webhook_type'] %}
|
||||
<option value="{{type}}">{{type}}</option>
|
||||
{%end%}
|
||||
{% end %}
|
||||
</select>
|
||||
<br>
|
||||
<br>
|
||||
<div class="form-group">
|
||||
<label for="name">{{ translate('webhooks', 'name' , data['lang']) }}</label>
|
||||
<input type="input" class="form-control" name="name" id="name_input"
|
||||
value="{{ data['webhook']['name']}}" maxlength="30" placeholder="Name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="url">{{ translate('webhooks', 'url', data['lang']) }}</label>
|
||||
<input type="input" class="form-control" name="url" id="url"
|
||||
value="{{ data['webhook']['url']}}" placeholder="https://webhooks.craftycontrol.com/fakeurl" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="bot_name">{{ translate('webhooks', 'bot_name' , data['lang']) }}</label>
|
||||
<input type="input" class="form-control" name="bot_name" id="bot_name_input"
|
||||
value="{{ data['webhook']['bot_name']}}" maxlength="30" placeholder="Crafty Controller" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="trigger">{{ translate('webhooks', 'trigger', data['lang']) }}</label>
|
||||
<select class="form-control selectpicker show-tick" name="trigger" id="trigger-select" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
|
||||
{% for trigger in data['triggers'] %}
|
||||
{% if trigger in data["webhook"]["trigger"] %}
|
||||
<option value="{{trigger}}" selected>{{translate('webhooks', trigger , data['lang'])}}</option>
|
||||
{% else %}
|
||||
<option value="{{trigger}}">{{translate('webhooks', trigger , data['lang'])}}</option>
|
||||
{% end %}
|
||||
{% end %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="body">{{ translate('webhooks', 'webhook_body', data['lang']) }}</label>
|
||||
<textarea id="body-input" name="body" rows="4" cols="50">
|
||||
{{ data["webhook"]["body"] }}
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="bot_name">{{ translate('webhooks', 'color' , data['lang']) }}</label>
|
||||
<input type="color" class="form-control" name="color" id="color" value='{{data["webhook"]["color"]}}'>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="enabled" name="enabled" {% if data['webhook']['enabled'] %}checked{%end%}
|
||||
value="1">
|
||||
<label for="enabled" class="custom-control-label">{{ translate('webhooks', 'enabled', data['lang']) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
|
||||
translate('serverConfig', 'save', data['lang']) }}</button>
|
||||
<button type="reset"
|
||||
onclick="location.href=`/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks`"
|
||||
class="btn btn-light"><i class="fas fa-times"></i> {{ translate('serverConfig', 'cancel',
|
||||
data['lang']) }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<style>
|
||||
.custom-control-input:checked~.custom-control-label::before {
|
||||
color: black !important;
|
||||
background-color: blueviolet !important;
|
||||
border-color: var(--outline) !important;
|
||||
}
|
||||
|
||||
.custom-control-label::before {
|
||||
background-color: white !important;
|
||||
top: calc(-0.2rem);
|
||||
}
|
||||
|
||||
.custom-switch .custom-control-label::after {
|
||||
top: calc(-0.125rem + 1px);
|
||||
}
|
||||
#body-input {
|
||||
background-color: var(--card-banner-bg);
|
||||
outline-color: var(--outline);
|
||||
color: var(--base-text);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<!-- content-wrapper ends -->
|
||||
|
||||
{% end %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(function () {
|
||||
$('.form-check-input').bootstrapToggle({
|
||||
on: '',
|
||||
off: ''
|
||||
});
|
||||
})
|
||||
|
||||
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||
function getCookie(name) {
|
||||
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||
return r ? r[1] : undefined;
|
||||
}
|
||||
|
||||
function replacer(key, value) {
|
||||
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
|
||||
if (typeof value == "boolean") {
|
||||
return value
|
||||
}
|
||||
console.log(key)
|
||||
if (key === "interval" && value === ""){
|
||||
return 0;
|
||||
}
|
||||
if (key === "command" && typeof(value === "integer")){
|
||||
return value.toString();
|
||||
}else {
|
||||
return (isNaN(value) ? value : +value);
|
||||
}
|
||||
} else {
|
||||
if (value === "" && key == "start_time"){
|
||||
return "00:00";
|
||||
}else{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const serverId = new URLSearchParams(document.location.search).get('id');
|
||||
const webhookId = new URLSearchParams(document.location.search).get('webhook_id');
|
||||
$(document).ready(function () {
|
||||
console.log("ready!");
|
||||
console.log('ready for JS!');
|
||||
$('.selectpicker').selectpicker("refresh");
|
||||
$("#new_webhook_form").on("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
var token = getCookie("_xsrf")
|
||||
let webhookForm = document.getElementById("new_webhook_form");
|
||||
let select_val = JSON.stringify($('#trigger-select').val());
|
||||
select_val = JSON.parse(select_val);
|
||||
|
||||
let formData = new FormData(webhookForm);
|
||||
//Create an object from the form data entries
|
||||
let formDataObject = Object.fromEntries(formData.entries());
|
||||
//We need to make sure these are sent regardless of whether or not they're checked
|
||||
formDataObject.enabled = $("#enabled").prop('checked');
|
||||
formDataObject.trigger = select_val;
|
||||
|
||||
console.log(formDataObject);
|
||||
// Format the plain form data as JSON
|
||||
let formDataJsonString = JSON.stringify(formDataObject, replacer);
|
||||
|
||||
let res = await fetch(`/api/v2/servers/${serverId}/webhook/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-XSRFToken': token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: formDataJsonString,
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
window.location.href = `/panel/server_detail?id=${serverId}&subpage=webhooks`;
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#webhook_form").on("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
var token = getCookie("_xsrf");
|
||||
let webhookForm = document.getElementById("webhook_form");
|
||||
let select_val = JSON.stringify($('#trigger-select').val());
|
||||
select_val = JSON.parse(select_val);
|
||||
|
||||
let formData = new FormData(webhookForm);
|
||||
//Create an object from the form data entries
|
||||
let formDataObject = Object.fromEntries(formData.entries());
|
||||
//We need to make sure these are sent regardless of whether or not they're checked
|
||||
formDataObject.enabled = $("#enabled").prop('checked');
|
||||
formDataObject.trigger = select_val;
|
||||
if(formDataObject.webhook_type != "Discord"){
|
||||
delete formDataObject.color
|
||||
}
|
||||
|
||||
console.log(formDataObject);
|
||||
// Format the plain form data as JSON
|
||||
let formDataJsonString = JSON.stringify(formDataObject, replacer);
|
||||
|
||||
let res = await fetch(`/api/v2/servers/${serverId}/webhook/${webhookId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-XSRFToken': token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: formDataJsonString,
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
window.location.href = `/panel/server_detail?id=${serverId}&subpage=webhooks`;
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function hexToDiscordInt(hexColor) {
|
||||
// Remove the hash at the start if it's there
|
||||
const sanitizedHex = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
|
||||
|
||||
// Convert the hex to an integer
|
||||
return parseInt(sanitizedHex, 16);
|
||||
}
|
||||
</script>
|
||||
<script src="../../static/assets/vendors/js/bootstrap-select.min.js"></script>
|
||||
|
||||
{% end %}
|
390
app/frontend/templates/panel/server_webhooks.html
Normal file
@ -0,0 +1,390 @@
|
||||
{% extends ../base.html %}
|
||||
|
||||
{% block meta %}
|
||||
{% end %}
|
||||
|
||||
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Page Title Header Starts-->
|
||||
<div class="row page-title-header">
|
||||
<div class="col-12">
|
||||
<div class="page-header">
|
||||
<h4 class="page-title">
|
||||
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
|
||||
data['server_stats']['server_id']['server_name'] }}
|
||||
<br />
|
||||
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Page Title Header Ends-->
|
||||
|
||||
{% include "parts/details_stats.html" %}
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12 grid-margin">
|
||||
<div class="card">
|
||||
<div class="card-body pt-0">
|
||||
|
||||
<span class="d-none d-sm-block">
|
||||
{% include "parts/server_controls_list.html" %}
|
||||
</span>
|
||||
<span class="d-block d-sm-none">
|
||||
{% include "parts/m_server_controls_list.html" %}
|
||||
</span>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12" style="overflow-x:auto;">
|
||||
<div class="card">
|
||||
<div class="card-header header-sm d-flex justify-content-between align-items-center">
|
||||
<h4 class="card-title"><i class="fa-regular fa-bell"></i> {{ translate('webhooks', 'webhooks', data['lang']) }} </h4>
|
||||
{% if data['user_data']['hints'] %}
|
||||
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" , data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" , data-placement="bottom"></span>
|
||||
{% end %}
|
||||
<div><a class="btn btn-info" href="/panel/add_webhook?id={{ data['server_stats']['server_id']['server_id'] }}"><i class="fas fa-plus-circle"></i> {{ translate('webhooks', 'new', data['lang']) }}</a></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if len(data['webhooks']) == 0 %}
|
||||
<div style="text-align: center; color: grey;">
|
||||
<h7>{{ translate('webhooks', 'no-webhook', data['lang']) }} <strong>{{ translate('webhooks', 'newWebhook',data['lang']) }}</strong>.</h7>
|
||||
</div>
|
||||
{% end %}
|
||||
{% if len(data['webhooks']) > 0 %}
|
||||
<div class="d-none d-lg-block">
|
||||
<table class="table table-hover responsive-table" aria-label="webhooks list" id="webhook_table" width="100%" style="table-layout:fixed;">
|
||||
<thead>
|
||||
<tr class="rounded">
|
||||
<th scope="col" style="width: 5%; min-width: 64px;">{{ translate('webhooks', 'enabled', data['lang']) }}</th>
|
||||
<th scope="col" style="width: 15%; min-width: 10px;">{{ translate('webhooks', 'name', data['lang']) }} </th>
|
||||
<th scope="col" style="width: 20%; min-width: 50px;">{{ translate('webhooks', 'type', data['lang']) }}</th>
|
||||
<th scope="col" style="width: 50%; min-width: 50px;">{{ translate('webhooks', 'trigger', data['lang']) }}</th>
|
||||
<th scope="col" style="width: 10%; min-width: 50px;">{{ translate('webhooks', 'edit', data['lang']) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for webhook in data['webhooks'] %}
|
||||
<tr>
|
||||
<td id="{{webhook.enabled}}" class="enabled">
|
||||
<button type="button" class="btn btn-sm btn-info btn-toggle webhook-custom-toggle" id="webhook_{{webhook.id}}" name="webhook_{{webhook.id}}" data-webhook-id="{{webhook.id}}" data-webhook-enabled="{{ 'true' if webhook.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
|
||||
<div class="handle"></div>
|
||||
</button>
|
||||
</td>
|
||||
<td id="{{webhook.name}}" class="id">
|
||||
<p>{{webhook.name}}</p>
|
||||
</td>
|
||||
<td id="{{webhook.webhook_type}}" class="type">
|
||||
<p>{{webhook.webhook_type}}</p>
|
||||
</td>
|
||||
<td id="{{webhook.trigger}}" class="trigger" style="overflow: scroll; max-width: 30px;">
|
||||
<ul>
|
||||
{% for trigger in webhook.trigger.split(",") %}
|
||||
{% if trigger in data["triggers"] %}
|
||||
<li>{{translate('webhooks', trigger , data['lang'])}}</li>
|
||||
{%end%}
|
||||
{%end%}
|
||||
</ul>
|
||||
</td>
|
||||
<td id="webhook_edit" class="action">
|
||||
<button onclick="window.location.href=`/panel/webhook_edit?id={{ data['server_stats']['server_id']['server_id'] }}&webhook_id={{webhook.id}}`" class="btn btn-info">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<button data-webhook={{ webhook.id }} class="btn btn-danger del_button">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button data-webhook={{ webhook.id }} data-toggle="tooltip" title="{{ translate('webhooks', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket">
|
||||
<i class="fa-solid fa-vial"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-block d-lg-none">
|
||||
<table aria-label="webhooks list" class="table table-hover responsive-table" id="webhook_table_mini" width="100%" style="table-layout:fixed;">
|
||||
<thead>
|
||||
<tr class="rounded">
|
||||
<th style="width: 20%; min-width: 64px;">{{ translate('webhooks', 'enabled',
|
||||
data['lang']) }}</th>
|
||||
<th style="width: 40%; min-width: 10px;">Name
|
||||
</th>
|
||||
<th style="width: 40%; min-width: 50px;">{{ translate('webhooks', 'edit', data['lang'])
|
||||
}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for webhook in data['webhooks'] %}
|
||||
<tr>
|
||||
<td id="{{webhook.enabled}}" class="enabled">
|
||||
<button type="button" class="btn btn-sm btn-info btn-toggle webhook-custom-toggle" id="webhook_{{webhook.id}}" name="webhook_{{webhook.id}}" data-webhook-id="{{webhook.id}}" data-webhook-enabled="{{ 'true' if webhook.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
|
||||
<div class="handle"></div>
|
||||
</button>
|
||||
</td>
|
||||
<td id="{{webhook.name}}" class="id">
|
||||
<p>{{webhook.name}}</p>
|
||||
</td>
|
||||
<td id="webhook_edit" class="action">
|
||||
<button onclick="window.location.href=`/panel/webhook_edit?id={{ data['server_stats']['server_id']['server_id'] }}&webhook_id={{webhook.id}}`" class="btn btn-info">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<button data-webhook={{ webhook.id }} class="btn btn-danger del_button">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button data-webhook={{ webhook.id }} data-toggle="tooltip" title="{{ translate('webhooks', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket">
|
||||
<i class="fa-solid fa-vial"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.popover-body {
|
||||
color: white !important;
|
||||
;
|
||||
}
|
||||
|
||||
.toggle-handle {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.toggle-on {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
td.enabled {
|
||||
vertical-align: middle;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
|
||||
.enabled .toggle-group .btn {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<style>
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
td::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
td {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
</style>
|
||||
<!-- content-wrapper ends -->
|
||||
|
||||
{% end %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
console.log('ready for JS!')
|
||||
$('#webhook_table').DataTable({
|
||||
'order': [4, 'asc'],
|
||||
}
|
||||
);
|
||||
$('#webhook_table_mini').DataTable({
|
||||
'order': [2, 'asc']
|
||||
}
|
||||
);
|
||||
document.getElementById('webhook_table_mini_wrapper').hidden = true;
|
||||
});
|
||||
$(document).ready(function () {
|
||||
$('[data-toggle="popover"]').popover();
|
||||
if ($(window).width() < 1000) {
|
||||
$('.too_small').popover("show");
|
||||
document.getElementById('webhook_table_wrapper').hidden = true;
|
||||
document.getElementById('webhook_table_mini_wrapper').hidden = false;
|
||||
}
|
||||
|
||||
});
|
||||
$(window).ready(function () {
|
||||
$('body').click(function () {
|
||||
$('.too_small').popover("hide");
|
||||
});
|
||||
});
|
||||
$(window).resize(function () {
|
||||
// This will execute whenever the window is resized
|
||||
if ($(window).width() < 1000) {
|
||||
$('.too_small').popover("show");
|
||||
document.getElementById('webhook_table_wrapper').hidden = true;
|
||||
document.getElementById('webhook_table_mini_wrapper').hidden = false;
|
||||
}
|
||||
else {
|
||||
$('.too_small').popover("hide");
|
||||
document.getElementById('webhook_table_wrapper').hidden = false;
|
||||
document.getElementById('webhook_table_mini_wrapper').hidden = true;
|
||||
} // New width
|
||||
});
|
||||
|
||||
function debounce(func, timeout = 300) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||
};
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.webhook-custom-toggle').each(function () {
|
||||
const enabled = JSON.parse(this.getAttribute('data-webhook-enabled'));
|
||||
if (enabled) {
|
||||
this.classList.add("active");
|
||||
}
|
||||
else {
|
||||
this.classList.remove("active");
|
||||
}
|
||||
})
|
||||
});
|
||||
$(() => {
|
||||
$('.webhook-custom-toggle').click(function () {
|
||||
const id = this.getAttribute('data-webhook-id');
|
||||
const enabled = !JSON.parse(this.getAttribute('data-webhook-enabled'));
|
||||
|
||||
fetch(`/api/v2/servers/{{data['server_id']}}/webhook/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ enabled }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
const serverId = new URLSearchParams(document.location.search).get('id')
|
||||
</script>
|
||||
<script>
|
||||
|
||||
|
||||
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||
function getCookie(name) {
|
||||
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||
return r ? r[1] : undefined;
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
console.log("ready!");
|
||||
|
||||
});
|
||||
|
||||
|
||||
$(".del_button").click(function () {
|
||||
var webhook_id = $(this).data('webhook');
|
||||
|
||||
bootbox.confirm({
|
||||
message: "{{ translate('webhooks', 'areYouSureDel', data['lang']) }}",
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data["lang"]) }}'
|
||||
},
|
||||
confirm: {
|
||||
className: 'btn-outline-danger',
|
||||
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data["lang"]) }}'
|
||||
}
|
||||
},
|
||||
callback: function (result) {
|
||||
console.log(result);
|
||||
if (result == true) {
|
||||
del_hook(webhook_id, serverId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".test-socket").click(function () {
|
||||
var webhook_id = $(this).data('webhook');
|
||||
|
||||
bootbox.confirm({
|
||||
message: "{{ translate('webhooks', 'areYouSureRun', data['lang']) }}",
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data["lang"]) }}'
|
||||
},
|
||||
confirm: {
|
||||
className: 'btn-outline-danger',
|
||||
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data["lang"]) }}'
|
||||
}
|
||||
},
|
||||
callback: function (result) {
|
||||
console.log(result);
|
||||
if (result == true) {
|
||||
test_hook(webhook_id, serverId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
async function test_hook(webhook_id, id) {
|
||||
var token = getCookie("_xsrf")
|
||||
|
||||
let res = await fetch(`/api/v2/servers/${id}/webhook/${webhook_id}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'token': token,
|
||||
},
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
bootbox.alert("Webhook Sent!")
|
||||
} else {
|
||||
console.log(responseData);
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function del_hook(webhook_id, id) {
|
||||
var token = getCookie("_xsrf")
|
||||
|
||||
let res = await fetch(`/api/v2/servers/${id}/webhook/${webhook_id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'token': token,
|
||||
},
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
window.location.reload();
|
||||
} else {
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{% end %}
|
@ -30,9 +30,6 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if data['running'] != 0 %}
|
||||
<span id="sync" style="margin-left: 5px;"><i class="fas fa-sync fa-spin"></i></span></h4>
|
||||
{% end %}
|
||||
{% for server in data['servers'] %}
|
||||
{% if server['server_data']['show_status'] %}
|
||||
<tr>
|
||||
@ -47,10 +44,14 @@
|
||||
</td>
|
||||
<td id="server_motd_{{ server['stats']['server_id']['server_id'] }}">
|
||||
{% if server['stats']['desc'] != 'False' %}
|
||||
<img src="/static/assets/images/pack.png" alt="icon"
|
||||
style="-webkit-filter:grayscale(100%); filter:grayscale(100%)" />
|
||||
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{
|
||||
server['stats']['desc'] }}</span> <br />
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<img src="/static/assets/images/pack.png" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%)" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{ server['stats']['desc'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
</td>
|
||||
<td id="server_version_{{ server['stats']['server_id']['server_id'] }}">
|
||||
@ -89,12 +90,9 @@
|
||||
</div>
|
||||
<!-- View for Small screen -->
|
||||
<div class="row justify-content-center align-items-sm-center">
|
||||
<div class="content-wrapper login-modal d-sm-none d-block" style="background-color: var(--dropdown-bg);">
|
||||
<div class="content-wrapper login-modal d-sm-none d-block" style="background-color: var(--dropdown-bg);">
|
||||
<img src="/static/assets/images/logo_long.png" style='width: 100%;'>
|
||||
<hr />
|
||||
{% if data['running'] != 0 %}
|
||||
<span id="m_sync" style="margin-left: 5px;"><i class="fas fa-sync fa-spin"></i></span></h4>
|
||||
{% end %}
|
||||
<div class="accordion" id="accordionServers">
|
||||
{% for server in data['servers'] %}
|
||||
{% if server['server_data']['show_status'] %}
|
||||
@ -103,17 +101,13 @@
|
||||
<h2 class="mb-0 container overflow-hidden">
|
||||
<div class="row">
|
||||
<div class="col-8 mx-0 px-0">
|
||||
<a id="m_server_name_{{ server['stats']['server_id']['server_id'] }}"
|
||||
class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse"
|
||||
data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false"
|
||||
aria-controls="collapse-{{server['server_data']['server_id']}}">
|
||||
<a id="m_server_name_{{ server['stats']['server_id']['server_id'] }}" class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse" data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false" aria-controls="collapse-{{server['server_data']['server_id']}}">
|
||||
<i class="fas fa-server"></i>
|
||||
{{ server['server_data']['server_name'] }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 mx-0 px-0">
|
||||
<a id="m_server_online_status_{{ server['stats']['server_id']['server_id'] }}"
|
||||
class="btn btn-link d-flex justify-content-center" type="button">
|
||||
<a id="m_server_online_status_{{ server['stats']['server_id']['server_id'] }}" class="btn btn-link d-flex justify-content-center" type="button">
|
||||
{% if server['stats']['running'] %}
|
||||
<div id="m_server_players_{{ server['stats']['server_id']['server_id'] }}">
|
||||
<span class="text-success"><i class="fas fa-signal"></i> {{ server['stats']['online'] }} / {{
|
||||
@ -129,14 +123,12 @@
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse"
|
||||
aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
|
||||
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse" aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
|
||||
<div class="card-body">
|
||||
{% if server['stats']['int_ping_results'] != 'False' %}
|
||||
<div id="m_server_motd_{{ server['stats']['server_id']['server_id'] }}" class="media">
|
||||
{% if server['stats']['desc'] != 'False' %}
|
||||
<img src="/static/assets/images/pack.png" class="w-25 mr-3" alt="icon"
|
||||
style="-webkit-filter:grayscale(100%); filter:grayscale(100%);" />
|
||||
<img src="/static/assets/images/pack.png" class="w-25 mr-3" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%);" />
|
||||
{% end %}
|
||||
<div class="media-body">
|
||||
{% if server['stats']['desc'] != 'False' %}
|
||||
@ -198,11 +190,9 @@
|
||||
m_server_online_status = document.getElementById('m_server_online_status_' + server.id);
|
||||
|
||||
/* TODO Update each element */
|
||||
if (server.int_ping_results) {
|
||||
document.getElementById('sync').innerHTML = '';
|
||||
document.getElementById('m_sync').innerHTML = '';
|
||||
if (server.running) {
|
||||
/* Update Players */
|
||||
if (server.players) {
|
||||
if (server.max != 0) {
|
||||
server_players.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />`
|
||||
}
|
||||
|
||||
@ -210,16 +200,18 @@
|
||||
var motd = "";
|
||||
if (server.desc) {
|
||||
if (server.icon) {
|
||||
motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
|
||||
img_motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
|
||||
m_motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
|
||||
}
|
||||
else {
|
||||
motd = `<img src="/static/assets/images/pack.png" alt="icon" /> `;
|
||||
img_motd = `<img src="/static/assets/images/pack.png" alt="icon" /> `;
|
||||
m_motd = `<img class="w-25 mr-3" src="/static/assets/images/pack.png" alt="icon" /> `;
|
||||
}
|
||||
|
||||
motd = motd + `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span> <br />`;
|
||||
desc_motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span> <br />`;
|
||||
m_motd = m_motd + `<div class="media-body"><span id="m_input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span></div>`;
|
||||
|
||||
motd = `<div class="row"><div class="col-auto">` + img_motd + `</div><div class="col-auto">` + desc_motd + `</div></div>`;
|
||||
server_motd.innerHTML = motd;
|
||||
m_server_motd.innerHTML = m_motd;
|
||||
}
|
||||
@ -252,17 +244,31 @@
|
||||
}
|
||||
|
||||
function update_servers_status(data) {
|
||||
console.log(data);
|
||||
update_one_server_status(data[0]);
|
||||
console.log("update servers");
|
||||
data.forEach(server => {
|
||||
console.log(server);
|
||||
update_one_server_status(server);
|
||||
});
|
||||
display_motd();
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
let xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var myData = JSON.parse(this.responseText);
|
||||
update_servers_status(myData.data);
|
||||
}
|
||||
};
|
||||
xmlHttp.open('GET', '/api/v2/servers/status', true);
|
||||
xmlHttp.send();
|
||||
|
||||
setTimeout(refreshStatus, 30000);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
console.log("ready!");
|
||||
|
||||
if (webSocket) {
|
||||
webSocket.on('update_server_status', update_servers_status);
|
||||
}
|
||||
refreshStatus();
|
||||
}());
|
||||
</script>
|
||||
|
||||
|
@ -1,167 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ data['lang_page'] }}" class="default">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
{% block meta %}{% end %}
|
||||
<title>{% block title %}{{ _('Default') }}{% end %}</title>
|
||||
<!-- plugins:css -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/assets/vendors/mdi/css/materialdesignicons.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/assets/vendors/ti-icons/css/themify-icons.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/assets/vendors/typicons/typicons.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/assets/vendors/css/vendor.bundle.base.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/assets/vendors/fontawesome6/css/all.css"
|
||||
/>
|
||||
<link rel="manifest" href="/static/assets/crafty.webmanifest" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="Crafty" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="../static/assets/images/Crafty_4-0.png"
|
||||
/>
|
||||
<!-- endinject -->
|
||||
<!-- Plugin css for this page -->
|
||||
<!-- End Plugin css for this page -->
|
||||
<!-- Layout styles -->
|
||||
<link rel="stylesheet" href="/static/assets/css/dark/style.css" />
|
||||
<!-- End Layout styles -->
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/svg+xml"
|
||||
href="/static/assets/images/logo_small.svg"
|
||||
/>
|
||||
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
|
||||
</head>
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
{% block meta %}{% end %}
|
||||
<title>{% block title %}{{ _('Default') }}{% end %}</title>
|
||||
<!-- plugins:css -->
|
||||
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css" />
|
||||
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css" />
|
||||
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css" />
|
||||
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css" />
|
||||
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css" />
|
||||
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css" />
|
||||
<link rel="manifest" href="/static/assets/crafty.webmanifest" />
|
||||
|
||||
<body>
|
||||
<div class="container-scroller">
|
||||
<div class="container-fluid page-body-wrapper full-page-wrapper">
|
||||
<div
|
||||
class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one"
|
||||
>
|
||||
<div class="mx-auto">
|
||||
<div class="auto-form-wrapper">{% block content %} {% end %}</div>
|
||||
</div>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="Crafty" />
|
||||
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png" />
|
||||
<!-- endinject -->
|
||||
<!-- Plugin css for this page -->
|
||||
<!-- End Plugin css for this page -->
|
||||
<!-- Layout styles -->
|
||||
<link rel="stylesheet" href="/static/assets/css/dark/style.css" />
|
||||
<!-- End Layout styles -->
|
||||
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg" />
|
||||
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-scroller">
|
||||
<div class="container-fluid page-body-wrapper full-page-wrapper">
|
||||
<div class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one">
|
||||
<div class="mx-auto">
|
||||
<div class="auto-form-wrapper">{% block content %} {% end %}</div>
|
||||
</div>
|
||||
<!-- content-wrapper ends -->
|
||||
</div>
|
||||
<!-- page-body-wrapper ends -->
|
||||
<!-- content-wrapper ends -->
|
||||
</div>
|
||||
<!-- container-scroller -->
|
||||
<!-- plugins:js -->
|
||||
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
|
||||
<!-- endinject -->
|
||||
<!-- inject:js -->
|
||||
<script src="/static/assets/js/shared/off-canvas.js"></script>
|
||||
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
|
||||
<script src="/static/assets/js/shared/misc.js"></script>
|
||||
<!-- endinject -->
|
||||
<script>
|
||||
// {% if request.protocol == 'https' %}
|
||||
let usingWebSockets = true;
|
||||
<!-- page-body-wrapper ends -->
|
||||
</div>
|
||||
<!-- page-body-wrapper ends -->
|
||||
|
||||
let listenEvents = [];
|
||||
<!-- container-scroller -->
|
||||
<!-- plugins:js -->
|
||||
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
|
||||
<!-- endinject -->
|
||||
<!-- inject:js -->
|
||||
<script src="/static/assets/js/shared/off-canvas.js"></script>
|
||||
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
|
||||
<script src="/static/assets/js/shared/misc.js"></script>
|
||||
<!-- endinject -->
|
||||
|
||||
let pageQueryParams, page;
|
||||
|
||||
try {
|
||||
pageQueryParams =
|
||||
"page_query_params=" + encodeURIComponent(location.search);
|
||||
page = "page=" + encodeURIComponent(location.pathname);
|
||||
var wsInternal = new WebSocket(
|
||||
"wss://" + location.host + "/ws?" + page + "&" + pageQueryParams
|
||||
);
|
||||
wsInternal.onopen = function () {
|
||||
console.log("opened WebSocket connection:", wsInternal);
|
||||
};
|
||||
wsInternal.onmessage = function (rawMessage) {
|
||||
var message = JSON.parse(rawMessage.data);
|
||||
|
||||
console.log("got message: ", message);
|
||||
|
||||
listenEvents
|
||||
.filter((listenedEvent) => listenedEvent.event == message.event)
|
||||
.forEach((listenedEvent) => listenedEvent.callback(message.data));
|
||||
};
|
||||
wsInternal.onerror = function (errorEvent) {
|
||||
console.error("WebSocket Error", errorEvent);
|
||||
};
|
||||
wsInternal.onclose = function (closeEvent) {
|
||||
console.log("Closed WebSocket", closeEvent);
|
||||
};
|
||||
|
||||
webSocket = {
|
||||
on: function (event, callback) {
|
||||
console.log("registered " + event + " event");
|
||||
listenEvents.push({ event: event, callback: callback });
|
||||
},
|
||||
emit: function (event, data) {
|
||||
var message = {
|
||||
event: event,
|
||||
data: data,
|
||||
};
|
||||
|
||||
wsInternal.send(JSON.stringify(message));
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error while making websocket helpers", error);
|
||||
usingWebSockets = false;
|
||||
{% block js %}
|
||||
<!-- Custom js for this page -->
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
let login_opacity_div = document.getElementById('login_opacity');
|
||||
let opacity = login_opacity_div.getAttribute('data-value');
|
||||
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
|
||||
//Register Service worker for mobile app
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', { scope: '/' })
|
||||
.then(function (registration) {
|
||||
console.error('Service Worker Registered');
|
||||
});
|
||||
}
|
||||
// {% else %}
|
||||
usingWebSockets = false;
|
||||
warn(
|
||||
"WebSockets are not supported in Crafty if not using the https protocol"
|
||||
);
|
||||
var webSocket;
|
||||
// {% end%}
|
||||
</script>
|
||||
{% block js %}
|
||||
<!-- Custom js for this page -->
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
let login_opacity_div = document.getElementById("login_opacity");
|
||||
let opacity = login_opacity_div.getAttribute("data-value");
|
||||
document.getElementById("login-form-background").style.background =
|
||||
"rgb(34, 36, 55, " + opacity / 100 + ")";
|
||||
//Register Service worker for mobile app
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/static/assets/js/shared/service-worker.js", {
|
||||
scope: "/",
|
||||
})
|
||||
.then(function (registration) {
|
||||
console.log("Service Worker Registered");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- End custom js for this page -->
|
||||
{% end %}
|
||||
</body>
|
||||
</html>
|
||||
});
|
||||
|
||||
</script>
|
||||
<!-- End custom js for this page -->
|
||||
{% end %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="content-wrapper">
|
||||
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
|
||||
<ul class="nav nav-pills tab-simple-styled " role="tablist">
|
||||
<li class="nav-item term-nav-item">
|
||||
<a class="nav-link" href="/server/step1" role="tab" aria-selected="false">
|
||||
<i class="fas fa-file-signature"></i>Minecraft-Java</a>
|
||||
@ -610,7 +610,9 @@
|
||||
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
|
||||
closeButton: false
|
||||
});
|
||||
getDirView();
|
||||
setTimeout(function(){
|
||||
getDirView();
|
||||
}, 2000);
|
||||
} else {
|
||||
bootbox.alert("You must input a path before selecting this button");
|
||||
}
|
||||
@ -664,7 +666,9 @@
|
||||
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
|
||||
closeButton: false
|
||||
});
|
||||
getDirView();
|
||||
setTimeout(function(){
|
||||
getDirView();
|
||||
}, 2000);
|
||||
} else {
|
||||
bootbox.alert("You must input a path before selecting this button");
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="content-wrapper">
|
||||
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
|
||||
<ul class="nav nav-pills tab-simple-styled">
|
||||
<li class="nav-item term-nav-item">
|
||||
<a class="nav-link active" href="/server/step1" role="tab" aria-selected="false">
|
||||
<i class="fas fa-file-signature"></i>Minecraft-Java</a>
|
||||
@ -846,8 +846,9 @@
|
||||
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
|
||||
closeButton: false
|
||||
});
|
||||
console.log("CALLING DIR")
|
||||
getDirView();
|
||||
setTimeout(function(){
|
||||
getDirView();
|
||||
}, 2000);
|
||||
} else {
|
||||
bootbox.alert("You must input a path before selecting this button");
|
||||
}
|
||||
@ -863,8 +864,9 @@
|
||||
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
|
||||
closeButton: false
|
||||
});
|
||||
console.log("CALLING DIR")
|
||||
getDirView();
|
||||
setTimeout(function(){
|
||||
getDirView();
|
||||
}, 2000);
|
||||
});
|
||||
var upload = false;
|
||||
var file;
|
||||
@ -1180,7 +1182,7 @@
|
||||
async function refreshCache() {
|
||||
document.getElementById("refresh-cache").classList.add("fa-spin")
|
||||
let token = getCookie("_xsrf")
|
||||
let res = await fetch(`/api/v2/crafty/exeCache`, {
|
||||
let res = await fetch(`/api/v2/crafty/JarCache`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
|
27
app/migrations/20230603_webhooks.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by database migrator
|
||||
import peewee
|
||||
|
||||
|
||||
def migrate(migrator, database, **kwargs):
|
||||
migrator.drop_columns("webhooks", ["name", "method", "url", "event", "send_data"])
|
||||
migrator.add_columns(
|
||||
"webhooks",
|
||||
server_id=peewee.IntegerField(null=True),
|
||||
webhook_type=peewee.CharField(default="Custom"),
|
||||
name=peewee.CharField(default="Custom Webhook", max_length=64),
|
||||
url=peewee.CharField(default=""),
|
||||
bot_name=peewee.CharField(default="Crafty Controller"),
|
||||
trigger=peewee.CharField(default="server_start,server_stop"),
|
||||
body=peewee.CharField(default=""),
|
||||
color=peewee.CharField(default=""),
|
||||
enabled=peewee.BooleanField(default=True),
|
||||
)
|
||||
"""
|
||||
Write your migrations here.
|
||||
"""
|
||||
|
||||
|
||||
def rollback(migrator, database, **kwargs):
|
||||
"""
|
||||
Write your rollback migrations here.
|
||||
"""
|
16
app/migrations/stats/20230827_add_icon.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by database migrator
|
||||
import peewee
|
||||
|
||||
|
||||
def migrate(migrator, database, **kwargs):
|
||||
migrator.add_columns("server_stats", icon=peewee.CharField(null=True))
|
||||
"""
|
||||
Write your migrations here.
|
||||
"""
|
||||
|
||||
|
||||
def rollback(migrator, database, **kwargs):
|
||||
migrator.drop_columns("server_stats", ["icon"])
|
||||
"""
|
||||
Write your rollback migrations here.
|
||||
"""
|