mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Merge branch 'dev' of github.com:bunkerity/bunkerweb into dev
This commit is contained in:
commit
cf8671655c
36 changed files with 1571 additions and 1951 deletions
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from sqlalchemy import TEXT, Boolean, Column, DateTime, Enum, ForeignKey, Identity, Integer, LargeBinary, String, UnicodeText
|
||||
from json import dumps, loads
|
||||
from typing import Any, Optional
|
||||
from sqlalchemy import TEXT, Boolean, Column, DateTime, Enum, ForeignKey, Identity, Integer, LargeBinary, String, Text, TypeDecorator, UnicodeText
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
|
||||
|
|
@ -310,6 +312,36 @@ class Metadata(Base):
|
|||
## UI Models
|
||||
|
||||
THEMES_ENUM = Enum("light", "dark", name="themes_enum")
|
||||
TABLES_ENUM = Enum("bans", "configs", "instances", "jobs", "plugins", "reports", "services", name="tables_enum")
|
||||
|
||||
|
||||
class JSONText(TypeDecorator):
|
||||
"""
|
||||
Custom JSON type to serialize/deserialize dictionaries as strings.
|
||||
Compatible with all databases (MariaDB, MySQL, PostgreSQL, SQLite).
|
||||
Ensures JSON strings are sorted by keys for consistent storage.
|
||||
"""
|
||||
|
||||
impl = Text # Stores JSON as a TEXT field in the database
|
||||
|
||||
def process_bind_param(self, value: Optional[dict], dialect: Any) -> Optional[str]:
|
||||
"""
|
||||
Convert a dictionary to a JSON string before saving to the database.
|
||||
Sorts dictionary keys for consistent serialization.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
# Serialize dictionary to a sorted JSON string
|
||||
return dumps(dict(sorted(value.items())))
|
||||
|
||||
def process_result_value(self, value: Optional[str], dialect: Any) -> Optional[dict]:
|
||||
"""
|
||||
Convert a JSON string back to a dictionary after retrieving from the database.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
# Deserialize JSON string to dictionary
|
||||
return loads(value)
|
||||
|
||||
|
||||
class Users(Base):
|
||||
|
|
@ -331,6 +363,7 @@ class Users(Base):
|
|||
roles = relationship("RolesUsers", back_populates="user", cascade="all")
|
||||
recovery_codes = relationship("UserRecoveryCodes", back_populates="user", cascade="all")
|
||||
sessions = relationship("UserSessions", back_populates="user", cascade="all")
|
||||
columns_preferences = relationship("UserColumnsPreferences", back_populates="user", cascade="all")
|
||||
list_roles: list[str] = []
|
||||
list_permissions: list[str] = []
|
||||
list_recovery_codes: list[str] = []
|
||||
|
|
@ -396,3 +429,15 @@ class UserSessions(Base):
|
|||
last_activity = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
user = relationship("Users", back_populates="sessions")
|
||||
|
||||
|
||||
class UserColumnsPreferences(Base):
|
||||
__tablename__ = "bw_ui_user_columns_preferences"
|
||||
__table_args__ = (UniqueConstraint("user_name", "table_name"),)
|
||||
|
||||
id = Column(Integer, Identity(start=1, increment=1), primary_key=True)
|
||||
user_name = Column(String(256), ForeignKey("bw_ui_users.username", onupdate="cascade", ondelete="cascade"), nullable=False)
|
||||
table_name = Column(TABLES_ENUM, nullable=False)
|
||||
columns = Column(JSONText, nullable=False)
|
||||
|
||||
user = relationship("Users", back_populates="columns_preferences")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from logging import Logger
|
|||
from os import sep
|
||||
from os.path import join
|
||||
from sys import path as sys_path
|
||||
from typing import List, Literal, Optional, Union
|
||||
from typing import Dict, List, Literal, Optional, Union
|
||||
|
||||
|
||||
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("api",), ("db",))]:
|
||||
|
|
@ -14,9 +14,10 @@ from bcrypt import gensalt, hashpw
|
|||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from Database import Database # type: ignore
|
||||
from model import Permissions, Roles, RolesPermissions, RolesUsers, UserRecoveryCodes, UserSessions # type: ignore
|
||||
from model import Permissions, Roles, RolesPermissions, RolesUsers, UserColumnsPreferences, UserRecoveryCodes, UserSessions # type: ignore
|
||||
|
||||
from app.models.models import UiUsers
|
||||
from app.utils import COLUMNS_PREFERENCES_DEFAULTS
|
||||
|
||||
|
||||
class UIDatabase(Database):
|
||||
|
|
@ -30,13 +31,20 @@ class UIDatabase(Database):
|
|||
query = session.query(UiUsers).filter_by(username=username)
|
||||
else:
|
||||
query = session.query(UiUsers).filter_by(admin=True)
|
||||
query = query.options(joinedload(UiUsers.roles), joinedload(UiUsers.recovery_codes))
|
||||
query = query.options(joinedload(UiUsers.roles), joinedload(UiUsers.recovery_codes), joinedload(UiUsers.columns_preferences))
|
||||
|
||||
ui_user = query.first()
|
||||
|
||||
if not ui_user:
|
||||
return None
|
||||
elif not as_dict:
|
||||
|
||||
if not ui_user.columns_preferences and not self.readonly:
|
||||
for table_name, columns in COLUMNS_PREFERENCES_DEFAULTS.items():
|
||||
session.add(UserColumnsPreferences(user_name=ui_user.username, table_name=table_name, columns=columns))
|
||||
session.commit()
|
||||
session.refresh(ui_user)
|
||||
|
||||
if not as_dict:
|
||||
return ui_user
|
||||
|
||||
ui_user_data = {
|
||||
|
|
@ -102,6 +110,9 @@ class UIDatabase(Database):
|
|||
for code in totp_recovery_codes or []:
|
||||
session.add(UserRecoveryCodes(user_name=username, code=hashpw(code.encode("utf-8"), gensalt(rounds=10)).decode("utf-8")))
|
||||
|
||||
for table_name, columns in COLUMNS_PREFERENCES_DEFAULTS.items():
|
||||
session.add(UserColumnsPreferences(user_name=username, table_name=table_name, columns=columns))
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
|
|
@ -141,6 +152,7 @@ class UIDatabase(Database):
|
|||
session.query(RolesUsers).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserRecoveryCodes).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserSessions).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserColumnsPreferences).filter_by(user_name=old_username).update({"user_name": username})
|
||||
|
||||
totp_changed = user.totp_secret != totp_secret
|
||||
|
||||
|
|
@ -176,6 +188,7 @@ class UIDatabase(Database):
|
|||
|
||||
session.query(RolesUsers).filter_by(user_name=username).delete()
|
||||
session.query(UserRecoveryCodes).filter_by(user_name=username).delete()
|
||||
session.query(UserColumnsPreferences).filter_by(user_name=username).delete()
|
||||
session.delete(user)
|
||||
|
||||
try:
|
||||
|
|
@ -417,3 +430,35 @@ class UIDatabase(Database):
|
|||
return str(e)
|
||||
|
||||
return ""
|
||||
|
||||
def update_ui_user_columns_preferences(self, username: str, table_name: str, columns: Dict[str, bool]) -> str:
|
||||
"""Update ui user columns preferences."""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
user = session.query(UiUsers).filter_by(username=username).first()
|
||||
if not user:
|
||||
return f"User {username} doesn't exist"
|
||||
|
||||
columns_preferences = session.query(UserColumnsPreferences).filter_by(user_name=username, table_name=table_name).first()
|
||||
if not columns_preferences:
|
||||
return f"Table {table_name} doesn't exist"
|
||||
|
||||
columns_preferences.columns = columns
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
return str(e)
|
||||
|
||||
return ""
|
||||
|
||||
def get_ui_user_columns_preferences(self, username: str, table_name: str) -> Dict[str, bool]:
|
||||
"""Get ui user columns preferences."""
|
||||
with self._db_session() as session:
|
||||
columns_preferences = session.query(UserColumnsPreferences).filter_by(user_name=username, table_name=table_name).first()
|
||||
if not columns_preferences:
|
||||
return COLUMNS_PREFERENCES_DEFAULTS.get(table_name, {})
|
||||
|
||||
return columns_preferences.columns
|
||||
|
|
|
|||
17
src/ui/app/static/js/common.js
Normal file
17
src/ui/app/static/js/common.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// common.js
|
||||
|
||||
/**
|
||||
* Debounce function to limit the rate at which a function can fire.
|
||||
* @param {Function} func - The function to debounce.
|
||||
* @param {number} delay - The delay in milliseconds.
|
||||
* @returns {Function} Debounced function.
|
||||
*/
|
||||
function debounce(func, delay) {
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
183
src/ui/app/static/js/dataTableInit.js
Normal file
183
src/ui/app/static/js/dataTableInit.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// dataTableInit.js
|
||||
|
||||
function initializeDataTable(config) {
|
||||
const {
|
||||
tableSelector,
|
||||
tableName,
|
||||
columnVisibilityCondition,
|
||||
dataTableOptions,
|
||||
} = config;
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
const searchPanesContainer = dataTable.searchPanes.container();
|
||||
if (!searchPanesContainer) return;
|
||||
dataTable.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize DataTable
|
||||
const dataTable = new DataTable(tableSelector, dataTableOptions);
|
||||
|
||||
if (dataTable.searchPanes.container())
|
||||
dataTable.searchPanes.container().hide();
|
||||
|
||||
$(".dt-type-numeric").removeClass("dt-type-numeric");
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$(tableSelector).removeClass("d-none");
|
||||
$(`#${tableName}-waiting`).addClass("visually-hidden");
|
||||
|
||||
const $columnsPreferenceDefaults = $("#columns_preferences_defaults");
|
||||
const $columnsPreference = $("#columns_preferences");
|
||||
|
||||
if ($columnsPreferenceDefaults.length && $columnsPreference.length) {
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$columnsPreferenceDefaults.val().trim(),
|
||||
);
|
||||
|
||||
// Handle column visibility preferences
|
||||
let columnVisibility = localStorage.getItem(`bw-${tableName}-columns`);
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse($columnsPreference.val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
dataTable.column(key).visible(value);
|
||||
});
|
||||
|
||||
// Save column preferences
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", $("#csrf_token").val().trim());
|
||||
data.append("table_name", tableName);
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
const savePreferencesUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
|
||||
fetch(savePreferencesUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// Column visibility event
|
||||
dataTable.on("column-visibility.dt", function (e, settings, column, state) {
|
||||
if (
|
||||
typeof columnVisibilityCondition === "function" &&
|
||||
!columnVisibilityCondition(column)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
columnVisibility[column] = state;
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
|
||||
if (isDefault) {
|
||||
localStorage.removeItem(`bw-${tableName}-columns`);
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
`bw-${tableName}-columns`,
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on("hidden.bs.toast", ".toast", function (event) {
|
||||
if (event.target.id.startsWith("feedback-toast")) {
|
||||
setTimeout(() => {
|
||||
$(this).remove();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
if (dataTable.responsive) dataTable.responsive.recalc();
|
||||
|
||||
dataTable.on("mouseenter", "td", function () {
|
||||
if (dataTable.cell(this).index() === undefined) return;
|
||||
const rowIdx = dataTable.cell(this).index().row;
|
||||
|
||||
dataTable
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
dataTable
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (dataTable.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
dataTable.on("mouseleave", "td", function () {
|
||||
dataTable
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
dataTable.on("select", function (e, dt, type, indexes) {
|
||||
const actionButton = $(".action-button");
|
||||
if (!actionButton.length) return;
|
||||
|
||||
// Enable the actions button
|
||||
actionButton
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
dataTable.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (dataTable.rows({ selected: true }).count() === 0) {
|
||||
const actionButton = $(".action-button");
|
||||
if (!actionButton.length) return;
|
||||
|
||||
actionButton
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
return dataTable;
|
||||
}
|
||||
|
|
@ -136,14 +136,6 @@ $(document).ready(function () {
|
|||
$("#selected-ips-input-unban").val(JSON.stringify(bans));
|
||||
};
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const layout = {
|
||||
top1: {
|
||||
searchPanes: {
|
||||
|
|
@ -289,15 +281,6 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
bans_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.unban_ips = {
|
||||
text: '<span class="tf-icons bx bxs-buoy bx-18px me-2"></span>Unban',
|
||||
action: function (e, dt, node, config) {
|
||||
|
|
@ -323,259 +306,163 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
const bans_table = new DataTable("#bans", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{ type: "ip-address", targets: 2 },
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
targets: [2, 6],
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
initializeDataTable({
|
||||
tableSelector: "#bans",
|
||||
tableName: "bans",
|
||||
columnVisibilityCondition: (column) => column > 2 && column < 8,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{ type: "ip-address", targets: 2 },
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
targets: [2, 6],
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "More than 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date >= 30 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "More than 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[2]);
|
||||
const now = new Date();
|
||||
return now - date >= 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 2,
|
||||
},
|
||||
targets: 2,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Next 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now < 24 * 60 * 60 * 1000;
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Next 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now < 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Next 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now < 7 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Next 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now < 7 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Next 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now < 30 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Next 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now < 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "More than 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now >= 30 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "More than 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[6]);
|
||||
const now = new Date();
|
||||
return date - now >= 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
searchPanes: { show: true },
|
||||
targets: 5,
|
||||
},
|
||||
],
|
||||
order: [[6, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ bans",
|
||||
infoEmpty: "No bans available",
|
||||
infoFiltered: "(filtered from _MAX_ total bans)",
|
||||
lengthMenu: "Display _MENU_ bans",
|
||||
zeroRecords: "No matching bans found",
|
||||
{
|
||||
searchPanes: { show: true },
|
||||
targets: 5,
|
||||
},
|
||||
],
|
||||
order: [[6, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d bans",
|
||||
0: "No bans selected",
|
||||
1: "Selected 1 ban",
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ bans",
|
||||
infoEmpty: "No bans available",
|
||||
infoFiltered: "(filtered from _MAX_ total bans)",
|
||||
lengthMenu: "Display _MENU_ bans",
|
||||
zeroRecords: "No matching bans found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d bans",
|
||||
0: "No bans selected",
|
||||
1: "Selected 1 ban",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#bans_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#bans_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot add bans.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#bans_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#bans_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot add bans.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
},
|
||||
});
|
||||
|
||||
bans_table.searchPanes.container().hide();
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$("#bans").removeClass("d-none");
|
||||
$("#bans-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-bans-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
bans_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
bans_table.responsive.recalc();
|
||||
|
||||
bans_table.on("mouseenter", "td", function () {
|
||||
if (bans_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = bans_table.cell(this).index().row;
|
||||
|
||||
bans_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
bans_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (bans_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
bans_table.on("mouseleave", "td", function () {
|
||||
bans_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
bans_table.on("select", function (e, dt, type, indexes) {
|
||||
// Enable the actions button
|
||||
$(".action-button")
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
bans_table.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (bans_table.rows({ selected: true }).count() === 0) {
|
||||
$(".action-button")
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
bans_table.on("column-visibility.dt", function (e, settings, column, state) {
|
||||
if (column < 3 || column === 8) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-bans-columns");
|
||||
} else {
|
||||
localStorage.setItem("bw-bans-columns", JSON.stringify(columnVisibility));
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", ".unban-ip", function () {
|
||||
|
|
|
|||
|
|
@ -123,113 +123,101 @@ $(document).ready(function () {
|
|||
},
|
||||
];
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: `<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters"${
|
||||
cacheJobNameSelection || cachePluginSelection || cacheServiceSelection
|
||||
? ' class="d-none"'
|
||||
: ""
|
||||
}>Show</span><span id="hide-filters"${
|
||||
!cacheJobNameSelection && !cachePluginSelection && !cacheServiceSelection
|
||||
? ' class="d-none"'
|
||||
: ""
|
||||
}>Hide</span><span class="d-none d-md-inline"> filters</span>`,
|
||||
action: function (e, dt, node, config) {
|
||||
cache_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
const cache_table = new DataTable("#cache", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
targets: 5,
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
},
|
||||
targets: [2, 3],
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: servicesSearchPanesOptions,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[5]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[5]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[5]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
const cache_table = initializeDataTable({
|
||||
tableSelector: "#cache",
|
||||
tableName: "cache",
|
||||
columnVisibilityCondition: (column) => 2 < column && column < 7,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
targets: 5,
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
targets: 5,
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
},
|
||||
targets: [2, 3],
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: servicesSearchPanesOptions,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[5]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[5]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[5]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
],
|
||||
order: [[3, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ cache files",
|
||||
infoEmpty: "No cache files available",
|
||||
infoFiltered: "(filtered from _MAX_ total cache files)",
|
||||
lengthMenu: "Display _MENU_ cache files",
|
||||
zeroRecords: "No matching cache files found",
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#cache_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
],
|
||||
order: [[3, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ cache files",
|
||||
infoEmpty: "No cache files available",
|
||||
infoFiltered: "(filtered from _MAX_ total cache files)",
|
||||
lengthMenu: "Display _MENU_ cache files",
|
||||
zeroRecords: "No matching cache files found",
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#cache_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -245,71 +233,9 @@ $(document).ready(function () {
|
|||
"click",
|
||||
);
|
||||
|
||||
if (!cacheJobNameSelection && !cachePluginSelection && !cacheServiceSelection)
|
||||
cache_table.searchPanes.container().hide();
|
||||
|
||||
$("#cache").removeClass("d-none");
|
||||
$("#cache-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: false,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-cache-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
cache_table.column(key).visible(value);
|
||||
});
|
||||
if (cacheJobNameSelection || cachePluginSelection || cacheServiceSelection) {
|
||||
cache_table.searchPanes.container().show();
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
}
|
||||
|
||||
cache_table.responsive.recalc();
|
||||
|
||||
cache_table.on("mouseenter", "td", function () {
|
||||
if (cache_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = cache_table.cell(this).index().row;
|
||||
|
||||
cache_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
cache_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (cache_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
cache_table.on("mouseleave", "td", function () {
|
||||
cache_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
cache_table.on("column-visibility.dt", function (e, settings, column, state) {
|
||||
if (column < 3 || column === 7) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-cache-columns");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"bw-cache-columns",
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,14 +65,6 @@ $(document).ready(function () {
|
|||
const $serviceDropdownItems = $("#services-dropdown-menu li.nav-item");
|
||||
const $typeDropdownItems = $("#types-dropdown-menu li.nav-item");
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const changeTypesVisibility = () => {
|
||||
$typeDropdownItems.each(function () {
|
||||
const item = $(this);
|
||||
|
|
|
|||
|
|
@ -263,19 +263,6 @@ $(document).ready(function () {
|
|||
return configs;
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: `<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters"${
|
||||
configTypeSelection || configServiceSelection ? ' class="d-none"' : ""
|
||||
}>Show</span><span id="hide-filters"${
|
||||
!configTypeSelection && !configServiceSelection ? ' class="d-none"' : ""
|
||||
}>Hide</span><span class="d-none d-md-inline"> filters</span>`,
|
||||
action: function (e, dt, node, config) {
|
||||
configs_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.create_config = {
|
||||
text: '<span class="tf-icons bx bx-plus"></span><span class="d-none d-md-inline"> Create new custom config</span>',
|
||||
className: `btn btn-sm rounded me-4 btn-bw-green${
|
||||
|
|
@ -315,148 +302,155 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
const configs_table = new DataTable("#configs", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: 7,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-window-alt"></i>HTTP',
|
||||
value: function (rowData, rowIdx) {
|
||||
$(rowData[3]).text().trim() === "HTTP";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-window-alt"></i>SERVER_HTTP',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "SERVER_HTTP";
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-window-alt"></i>DEFAULT_SERVER_HTTP',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "DEFAULT_SERVER_HTTP";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-shield-quarter"></i>MODSEC_CRS',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "MODSEC_CRS";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-shield-alt-2"></i>MODSEC',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "MODSEC";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-network-chart"></i>STREAM',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "STREAM";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-network-chart"></i>SERVER_STREAM',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "SERVER_STREAM";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-shield-alt"></i>CRS_PLUGINS_BEFORE',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "CRS_PLUGINS_BEFORE";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-shield-alt"></i>CRS_PLUGINS_AFTER',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "CRS_PLUGINS_AFTER";
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
const configs_table = initializeDataTable({
|
||||
tableSelector: "#configs",
|
||||
tableName: "configs",
|
||||
columnVisibilityCondition: (column) => column > 2 && column < 8,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: servicesSearchPanesOptions,
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: templatesSearchPanesOptions,
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
],
|
||||
order: [[2, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ custom configs",
|
||||
infoEmpty: "No custom configs available",
|
||||
infoFiltered: "(filtered from _MAX_ total custom configs)",
|
||||
lengthMenu: "Display _MENU_ custom configs",
|
||||
zeroRecords: "No matching custom configs found",
|
||||
{
|
||||
visible: false,
|
||||
targets: 7,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-window-alt"></i>HTTP',
|
||||
value: function (rowData, rowIdx) {
|
||||
$(rowData[3]).text().trim() === "HTTP";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-window-alt"></i>SERVER_HTTP',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "SERVER_HTTP";
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-window-alt"></i>DEFAULT_SERVER_HTTP',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "DEFAULT_SERVER_HTTP";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-shield-quarter"></i>MODSEC_CRS',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "MODSEC_CRS";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-shield-alt-2"></i>MODSEC',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "MODSEC";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-network-chart"></i>STREAM',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "STREAM";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-network-chart"></i>SERVER_STREAM',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "SERVER_STREAM";
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-shield-alt"></i>CRS_PLUGINS_BEFORE',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "CRS_PLUGINS_BEFORE";
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-shield-alt"></i>CRS_PLUGINS_AFTER',
|
||||
value: function (rowData, rowIdx) {
|
||||
return $(rowData[3]).text().trim() === "CRS_PLUGINS_AFTER";
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: servicesSearchPanesOptions,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: templatesSearchPanesOptions,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
],
|
||||
order: [[2, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d custom configs",
|
||||
0: "No custom configs selected",
|
||||
1: "Selected 1 custom config",
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ custom configs",
|
||||
infoEmpty: "No custom configs available",
|
||||
infoFiltered: "(filtered from _MAX_ total custom configs)",
|
||||
lengthMenu: "Display _MENU_ custom configs",
|
||||
zeroRecords: "No matching custom configs found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d custom configs",
|
||||
0: "No custom configs selected",
|
||||
1: "Selected 1 custom config",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#configs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#configs_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create new custom configurations.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
initComplete: function (settings, json) {
|
||||
$("#configs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#configs_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create new custom configurations.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -468,114 +462,12 @@ $(document).ready(function () {
|
|||
"click",
|
||||
);
|
||||
|
||||
if (!configTypeSelection && !configServiceSelection)
|
||||
configs_table.searchPanes.container().hide();
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$("#configs").removeClass("d-none");
|
||||
$("#configs-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
7: false,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-configs-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
configs_table.column(key).visible(value);
|
||||
});
|
||||
if (configTypeSelection || configServiceSelection) {
|
||||
configs_table.searchPanes.container().show();
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
}
|
||||
|
||||
configs_table.responsive.recalc();
|
||||
|
||||
configs_table.on("mouseenter", "td", function () {
|
||||
if (configs_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = configs_table.cell(this).index().row;
|
||||
|
||||
configs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
configs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (configs_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
configs_table.on("mouseleave", "td", function () {
|
||||
configs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
configs_table.on("select", function (e, dt, type, indexes) {
|
||||
// Enable the actions button
|
||||
$(".action-button")
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
configs_table.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (configs_table.rows({ selected: true }).count() === 0) {
|
||||
$(".action-button")
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
configs_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
if (column === 0 || column === 1 || column === 8) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-configs-columns");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"bw-configs-columns",
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$(document).on("click", ".delete-config", function () {
|
||||
if (isReadOnly) {
|
||||
alert("This action is not allowed in read-only mode.");
|
||||
|
|
|
|||
|
|
@ -263,14 +263,6 @@ $(document).ready(function () {
|
|||
},
|
||||
];
|
||||
|
||||
$(document).on("hidden.bs.toast", ".toast", function (event) {
|
||||
if (event.target.id.startsWith("feedback-toast")) {
|
||||
setTimeout(() => {
|
||||
$(this).remove();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
$("#modal-delete-instances").on("hidden.bs.modal", function () {
|
||||
$("#selected-instances").empty();
|
||||
$("#selected-instances-input").val("");
|
||||
|
|
@ -303,15 +295,6 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
instances_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.ping_instances = {
|
||||
text: '<span class="tf-icons bx bx-bell bx-18px me-2"></span>Ping',
|
||||
action: function (e, dt, node, config) {
|
||||
|
|
@ -382,318 +365,215 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
const instances_table = new DataTable("#instances", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: [3, 4],
|
||||
},
|
||||
{
|
||||
targets: [7, 8],
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
initializeDataTable({
|
||||
tableSelector: "#instances",
|
||||
tableName: "instances",
|
||||
columnVisibilityCondition: (column) => column > 2 && column < 9,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: [3, 4],
|
||||
},
|
||||
{
|
||||
targets: [7, 8],
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: [4],
|
||||
},
|
||||
targets: [4],
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-up-arrow-alt text-success"></i> Up',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("Up");
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-up-arrow-alt text-success"></i> Up',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("Up");
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-down-arrow-alt text-danger"></i> Down',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("Down");
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-down-arrow-alt text-danger"></i> Down',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("Down");
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bxs-hourglass text-warning"></i> Loading',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("Loading");
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bxs-hourglass text-warning"></i> Loading',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("Loading");
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-microchip"></i> Static',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("Static");
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-microchip"></i> Static',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("Static");
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bxl-docker"></i> Container',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("Container");
|
||||
{
|
||||
label: '<i class="bx bx-xs bxl-docker"></i> Container',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("Container");
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bxl-kubernetes"></i> Pod',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("Pod");
|
||||
{
|
||||
label: '<i class="bx bx-xs bxl-kubernetes"></i> Pod',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("Pod");
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[7]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[7]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[7]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[7]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[7]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[7]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 7,
|
||||
},
|
||||
targets: 7,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[8]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[8]);
|
||||
const now = new Date();
|
||||
return now - date < 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[8]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[8]);
|
||||
const now = new Date();
|
||||
return now - date < 7 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[8]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: function (rowData, rowIdx) {
|
||||
const date = new Date(rowData[8]);
|
||||
const now = new Date();
|
||||
return now - date < 30 * 24 * 60 * 60 * 1000;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 8,
|
||||
},
|
||||
targets: 8,
|
||||
},
|
||||
],
|
||||
order: [[8, "desc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ instances",
|
||||
infoEmpty: "No instances available",
|
||||
infoFiltered: "(filtered from _MAX_ total instances)",
|
||||
lengthMenu: "Display _MENU_ instances",
|
||||
zeroRecords: "No matching instances found",
|
||||
],
|
||||
order: [[8, "desc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d instances",
|
||||
0: "No instances selected",
|
||||
1: "Selected 1 instance",
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ instances",
|
||||
infoEmpty: "No instances available",
|
||||
infoFiltered: "(filtered from _MAX_ total instances)",
|
||||
lengthMenu: "Display _MENU_ instances",
|
||||
zeroRecords: "No matching instances found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d instances",
|
||||
0: "No instances selected",
|
||||
1: "Selected 1 instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#instances_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#instances_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create new instances.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
initComplete: function (settings, json) {
|
||||
$("#instances_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#instances_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create new instances.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
instances_table.searchPanes.container().hide();
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$("#instances").removeClass("d-none");
|
||||
$("#instances-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: false,
|
||||
4: false,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
8: true,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-instances-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
instances_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
instances_table.responsive.recalc();
|
||||
|
||||
instances_table.on("mouseenter", "td", function () {
|
||||
if (instances_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = instances_table.cell(this).index().row;
|
||||
|
||||
instances_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
instances_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (instances_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
instances_table.on("mouseleave", "td", function () {
|
||||
instances_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
instances_table.on("select", function (e, dt, type, indexes) {
|
||||
// Enable the actions button
|
||||
$(".action-button")
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
instances_table.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (instances_table.rows({ selected: true }).count() === 0) {
|
||||
$(".action-button")
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
instances_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
if (column === 0 || column === 1 || column === 9) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-instances-columns");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"bw-instances-columns",
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$(document).on("click", ".ping-instance", function () {
|
||||
if (actionLock) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -151,15 +151,6 @@ $(document).ready(function () {
|
|||
form.appendTo("body").submit();
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
jobs_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.run_jobs = {
|
||||
text: '<span class="tf-icons bx bx-play bx-18px me-2"></span>Run selected jobs',
|
||||
action: function (e, dt, node, config) {
|
||||
|
|
@ -205,262 +196,168 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const jobs_table = new DataTable("#jobs", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Interval",
|
||||
options: [
|
||||
{
|
||||
label: "Every day",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("day");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Every hour",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("hour");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Every week",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("week");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Once",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("once");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
initializeDataTable({
|
||||
tableSelector: "#jobs",
|
||||
tableName: "jobs",
|
||||
columnVisibilityCondition: (column) => column > 2 && column < 8,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Reload",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> No',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-check text-success"></i> Yes',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("bx-check");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Async",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> No',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-check text-success"></i> Yes',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-check");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Last run state",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> Failed',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-check text-success"></i> Success',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("bx-check");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
targets: 7,
|
||||
},
|
||||
],
|
||||
order: [[3, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ jobs",
|
||||
infoEmpty: "No jobs available",
|
||||
infoFiltered: "(filtered from _MAX_ total jobs)",
|
||||
lengthMenu: "Display _MENU_ jobs",
|
||||
zeroRecords: "No matching jobs found",
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Interval",
|
||||
options: [
|
||||
{
|
||||
label: "Every day",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("day");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Every hour",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("hour");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Every week",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("week");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Once",
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[4].includes("once");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Reload",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> No',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-check text-success"></i> Yes',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[5].includes("bx-check");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Async",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> No',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-check text-success"></i> Yes',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-check");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Last run state",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> Failed',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-check text-success"></i> Success',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("bx-check");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 7,
|
||||
},
|
||||
],
|
||||
order: [[3, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d jobs",
|
||||
0: "No jobs selected",
|
||||
1: "Selected 1 job",
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ jobs",
|
||||
infoEmpty: "No jobs available",
|
||||
infoFiltered: "(filtered from _MAX_ total jobs)",
|
||||
lengthMenu: "Display _MENU_ jobs",
|
||||
zeroRecords: "No matching jobs found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d jobs",
|
||||
0: "No jobs selected",
|
||||
1: "Selected 1 job",
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#jobs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#jobs_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
},
|
||||
});
|
||||
|
||||
jobs_table.searchPanes.container().hide();
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$("#jobs").removeClass("d-none");
|
||||
$("#jobs-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-jobs-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
jobs_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
jobs_table.responsive.recalc();
|
||||
|
||||
jobs_table.on("mouseenter", "td", function () {
|
||||
if (jobs_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = jobs_table.cell(this).index().row;
|
||||
|
||||
jobs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
jobs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (jobs_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
jobs_table.on("mouseleave", "td", function () {
|
||||
jobs_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
jobs_table.on("select", function (e, dt, type, indexes) {
|
||||
// Enable the actions button
|
||||
$(".action-button")
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
jobs_table.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (jobs_table.rows({ selected: true }).count() === 0) {
|
||||
$(".action-button")
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
jobs_table.on("column-visibility.dt", function (e, settings, column, state) {
|
||||
if (column < 3 || column === 8) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-jobs-columns");
|
||||
} else {
|
||||
localStorage.setItem("bw-jobs-columns", JSON.stringify(columnVisibility));
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", ".show-history", function () {
|
||||
|
|
|
|||
|
|
@ -70,12 +70,17 @@ $(document).ready(function () {
|
|||
});
|
||||
}
|
||||
|
||||
new DataTable(this, {
|
||||
columnDefs: columnDefs,
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
order: [[parseInt(tableOrder.column), tableOrder.dir]],
|
||||
initializeDataTable({
|
||||
tableSelector: this,
|
||||
tableName: this.id,
|
||||
columnVisibilityCondition: (column) => false, // Ignore all columns
|
||||
dataTableOptions: {
|
||||
columnDefs: columnDefs,
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
order: [[parseInt(tableOrder.column), tableOrder.dir]],
|
||||
},
|
||||
});
|
||||
|
||||
$(this).removeClass("d-none");
|
||||
|
|
|
|||
|
|
@ -280,15 +280,6 @@ $(document).ready(function () {
|
|||
return plugins;
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
plugins_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.add_plugin = {
|
||||
text: '<span class="tf-icons bx bx-plus"></span><span class="d-none d-md-inline"> Add plugin(s)</span>',
|
||||
className: `btn btn-sm rounded me-4 btn-bw-green${
|
||||
|
|
@ -330,245 +321,144 @@ $(document).ready(function () {
|
|||
},
|
||||
};
|
||||
|
||||
const plugins_table = new DataTable("#plugins", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
targets: [2, 4],
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Stream Support",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> No',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-check text-success"></i> Yes',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-check");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-minus text-warning"></i> Partial',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-minus");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
initializeDataTable({
|
||||
tableSelector: "#plugins",
|
||||
tableName: "plugins",
|
||||
columnVisibilityCondition: (column) =>
|
||||
column > 1 && column !== 3 && column < 9,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: `<img src="${$("#pro_diamond_url")
|
||||
.val()
|
||||
.trim()}" alt="Pro plugin" width="16px" height="12.9125px" class="mb-1"> PRO`,
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("PRO");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-plug bx-xs"></i> External',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("EXTERNAL");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-cloud-upload bx-xs"></i> UI',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("UI");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-shield bx-xs"></i> Core',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("CORE");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
targets: 7,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
{
|
||||
orderable: false,
|
||||
targets: -1,
|
||||
},
|
||||
targets: 8,
|
||||
},
|
||||
],
|
||||
order: [[3, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ plugins",
|
||||
infoEmpty: "No plugins available",
|
||||
infoFiltered: "(filtered from _MAX_ total plugins)",
|
||||
lengthMenu: "Display _MENU_ plugins",
|
||||
zeroRecords: "No matching plugins found",
|
||||
{
|
||||
visible: false,
|
||||
targets: [2, 4],
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
header: "Stream Support",
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-x text-danger"></i> No',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-x");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-check text-success"></i> Yes',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-check");
|
||||
},
|
||||
},
|
||||
{
|
||||
label:
|
||||
'<i class="bx bx-xs bx-minus text-warning"></i> Partial',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[6].includes("bx-minus");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: `<img src="${$("#pro_diamond_url")
|
||||
.val()
|
||||
.trim()}" alt="Pro plugin" width="16px" height="12.9125px" class="mb-1"> PRO`,
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("PRO");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-plug bx-xs"></i> External',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("EXTERNAL");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-cloud-upload bx-xs"></i> UI',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("UI");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-shield bx-xs"></i> Core',
|
||||
value: function (rowData, rowIdx) {
|
||||
return rowData[7].includes("CORE");
|
||||
},
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 7,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 8,
|
||||
},
|
||||
],
|
||||
order: [[3, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d plugins",
|
||||
0: "No plugins selected",
|
||||
1: "Selected 1 plugin",
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ plugins",
|
||||
infoEmpty: "No plugins available",
|
||||
infoFiltered: "(filtered from _MAX_ total plugins)",
|
||||
lengthMenu: "Display _MENU_ plugins",
|
||||
zeroRecords: "No matching plugins found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d plugins",
|
||||
0: "No plugins selected",
|
||||
1: "Selected 1 plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
initComplete: function (settings, json) {
|
||||
$("#plugins_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#plugins_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create add plugins.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
initComplete: function (settings, json) {
|
||||
$("#plugins_wrapper .btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly)
|
||||
$("#plugins_wrapper .dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in readonly, therefore you cannot create add plugins.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
plugins_table.searchPanes.container().hide();
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$("#plugins").removeClass("d-none");
|
||||
$("#plugins-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
2: false,
|
||||
4: false,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
8: true,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-plugins-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
plugins_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
plugins_table.responsive.recalc();
|
||||
|
||||
plugins_table.on("mouseenter", "td", function () {
|
||||
if (plugins_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = plugins_table.cell(this).index().row;
|
||||
|
||||
plugins_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
plugins_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (plugins_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
plugins_table.on("mouseleave", "td", function () {
|
||||
plugins_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
plugins_table.on("select", function (e, dt, type, indexes) {
|
||||
// Enable the actions button
|
||||
$(".action-button")
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
plugins_table.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (plugins_table.rows({ selected: true }).count() === 0) {
|
||||
$(".action-button")
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
plugins_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
if (column === 0 || column === 1 || column === 9) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-plugins-columns");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"bw-plugins-columns",
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$(document).on("click", ".delete-plugin", function () {
|
||||
if (isReadOnly) {
|
||||
alert("This action is not allowed in read-only mode.");
|
||||
|
|
|
|||
|
|
@ -390,15 +390,6 @@ $(function () {
|
|||
},
|
||||
];
|
||||
|
||||
// Custom button for toggling filters
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: (e, dt, node, config) => {
|
||||
reports_table.searchPanes.container().slideToggle();
|
||||
$("#show-filters, #hide-filters").toggleClass("d-none");
|
||||
},
|
||||
};
|
||||
|
||||
// Custom button for auto-refresh
|
||||
let autoRefresh = false;
|
||||
const sessionAutoRefresh = sessionStorage.getItem("reportsAutoRefresh");
|
||||
|
|
@ -436,119 +427,72 @@ $(function () {
|
|||
};
|
||||
|
||||
// Initialize DataTable
|
||||
const reports_table = new DataTable("#reports", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{ orderable: false, targets: -1 },
|
||||
{ visible: false, targets: [4, 5, 6, 7, 10] },
|
||||
{ type: "ip-address", targets: 2 },
|
||||
{
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
const reports_table = initializeDataTable({
|
||||
tableSelector: "#reports",
|
||||
tableName: "reports",
|
||||
columnVisibilityCondition: (column) => column > 2 && column < 12,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{ orderable: false, targets: -1 },
|
||||
{ visible: false, targets: [4, 5, 6, 7, 10] },
|
||||
{ type: "ip-address", targets: 2 },
|
||||
{
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return data;
|
||||
},
|
||||
targets: 1,
|
||||
},
|
||||
targets: 1,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: countriesSearchPanesOptions,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
options: countriesSearchPanesOptions,
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
targets: 3,
|
||||
{
|
||||
searchPanes: { show: true },
|
||||
targets: [2, 4, 5, 6, 8, 9, 11],
|
||||
},
|
||||
],
|
||||
order: [[1, "desc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ reports",
|
||||
infoEmpty: "No reports available",
|
||||
infoFiltered: "(filtered from _MAX_ total reports)",
|
||||
lengthMenu: "Display _MENU_ reports",
|
||||
zeroRecords: "No matching reports found",
|
||||
},
|
||||
{
|
||||
searchPanes: { show: true },
|
||||
targets: [2, 4, 5, 6, 8, 9, 11],
|
||||
initComplete: () => {
|
||||
$("#reports_wrapper")
|
||||
.find(".btn-secondary")
|
||||
.removeClass("btn-secondary");
|
||||
updateCountryTooltips();
|
||||
},
|
||||
],
|
||||
order: [[1, "desc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ reports",
|
||||
infoEmpty: "No reports available",
|
||||
infoFiltered: "(filtered from _MAX_ total reports)",
|
||||
lengthMenu: "Display _MENU_ reports",
|
||||
zeroRecords: "No matching reports found",
|
||||
},
|
||||
initComplete: () => {
|
||||
$("#reports_wrapper").find(".btn-secondary").removeClass("btn-secondary");
|
||||
updateCountryTooltips();
|
||||
},
|
||||
});
|
||||
|
||||
$(".dt-type-numeric").removeClass("dt-type-numeric");
|
||||
|
||||
if (sessionAutoRefresh === "true") {
|
||||
toggleAutoRefresh();
|
||||
}
|
||||
|
||||
// Initially hide search panes
|
||||
reports_table.searchPanes.container().hide();
|
||||
|
||||
// Show the reports table and hide the loading indicator
|
||||
$("#reports").removeClass("d-none");
|
||||
$("#reports-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: false,
|
||||
5: false,
|
||||
6: false,
|
||||
7: false,
|
||||
8: true,
|
||||
9: true,
|
||||
10: false,
|
||||
11: true,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-reports-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
reports_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
reports_table.responsive.recalc();
|
||||
|
||||
// Update tooltips after table draw
|
||||
reports_table.on("draw.dt", updateCountryTooltips);
|
||||
|
||||
reports_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
if (column < 3) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-reports-columns");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"bw-reports-columns",
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const hashValue = location.hash;
|
||||
if (hashValue) {
|
||||
$("#dt-length-0").val(hashValue.replace("#", ""));
|
||||
|
|
|
|||
|
|
@ -227,15 +227,6 @@ $(function () {
|
|||
})
|
||||
.get();
|
||||
|
||||
$.fn.dataTable.ext.buttons.toggle_filters = {
|
||||
text: '<span class="tf-icons bx bx-filter bx-18px me-2"></span><span id="show-filters">Show</span><span id="hide-filters" class="d-none">Hide</span><span class="d-none d-md-inline"> filters</span>',
|
||||
action: function (e, dt, node, config) {
|
||||
services_table.searchPanes.container().slideToggle(); // Smoothly hide or show the container
|
||||
$("#show-filters").toggleClass("d-none"); // Toggle the visibility of the 'Show' span
|
||||
$("#hide-filters").toggleClass("d-none"); // Toggle the visibility of the 'Hide' span
|
||||
},
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.create_service = {
|
||||
text: '<span class="tf-icons bx bx-plus"></span><span class="d-none d-md-inline"> Create new service</span>',
|
||||
className: `btn btn-sm rounded me-4 btn-bw-green${
|
||||
|
|
@ -334,255 +325,158 @@ $(function () {
|
|||
},
|
||||
};
|
||||
|
||||
const services_table = new DataTable("#services", {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{ orderable: false, targets: -1 },
|
||||
{
|
||||
targets: [5, 6],
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
initializeDataTable({
|
||||
tableSelector: "#services",
|
||||
tableName: "services",
|
||||
columnVisibilityCondition: (column) => column > 2 && column < 7,
|
||||
dataTableOptions: {
|
||||
columnDefs: [
|
||||
{
|
||||
orderable: false,
|
||||
className: "dtr-control",
|
||||
targets: 0,
|
||||
},
|
||||
{
|
||||
orderable: false,
|
||||
render: DataTable.render.select(),
|
||||
targets: 1,
|
||||
},
|
||||
{ orderable: false, targets: -1 },
|
||||
{
|
||||
targets: [5, 6],
|
||||
render: function (data, type, row) {
|
||||
if (type === "display" || type === "filter") {
|
||||
const date = new Date(data);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-globe"></i> Online',
|
||||
value: (rowData) => rowData[3].includes("Online"),
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-file-blank"></i> Draft',
|
||||
value: (rowData) => rowData[3].includes("Draft"),
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-globe"></i> Online',
|
||||
value: (rowData) => rowData[3].includes("Online"),
|
||||
},
|
||||
{
|
||||
label: '<i class="bx bx-xs bx-file-blank"></i> Draft',
|
||||
value: (rowData) => rowData[3].includes("Draft"),
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
targets: 3,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
targets: 4,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: (rowData) => new Date() - new Date(rowData[5]) < 86400000,
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: (rowData) => new Date() - new Date(rowData[5]) < 604800000,
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[5]) < 2592000000,
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[5]) < 86400000,
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[5]) < 604800000,
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[5]) < 2592000000,
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
targets: 5,
|
||||
},
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: (rowData) => new Date() - new Date(rowData[6]) < 86400000,
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: (rowData) => new Date() - new Date(rowData[6]) < 604800000,
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[6]) < 2592000000,
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
{
|
||||
searchPanes: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[6]) < 86400000,
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[6]) < 604800000,
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: (rowData) =>
|
||||
new Date() - new Date(rowData[6]) < 2592000000,
|
||||
},
|
||||
],
|
||||
combiner: "or",
|
||||
orderable: false,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
targets: 6,
|
||||
},
|
||||
],
|
||||
order: [[2, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ services",
|
||||
infoEmpty: "No services available",
|
||||
infoFiltered: "(filtered from _MAX_ total services)",
|
||||
lengthMenu: "Display _MENU_ services",
|
||||
zeroRecords: "No matching services found",
|
||||
],
|
||||
order: [[2, "asc"]],
|
||||
autoFill: false,
|
||||
responsive: true,
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d services",
|
||||
0: "No services selected",
|
||||
1: "Selected 1 service",
|
||||
style: "multi+shift",
|
||||
selector: "td:nth-child(2)",
|
||||
headerCheckbox: true,
|
||||
},
|
||||
layout: layout,
|
||||
language: {
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ services",
|
||||
infoEmpty: "No services available",
|
||||
infoFiltered: "(filtered from _MAX_ total services)",
|
||||
lengthMenu: "Display _MENU_ services",
|
||||
zeroRecords: "No matching services found",
|
||||
select: {
|
||||
rows: {
|
||||
_: "Selected %d services",
|
||||
0: "No services selected",
|
||||
1: "Selected 1 service",
|
||||
},
|
||||
},
|
||||
searchPanes: {
|
||||
collapse: {
|
||||
0: '<span class="tf-icons bx bx-search bx-18px me-2"></span>Filters',
|
||||
_: '<span class="tf-icons bx bx-search bx-18px me-2"></span>Filters (%d)',
|
||||
},
|
||||
},
|
||||
},
|
||||
searchPanes: {
|
||||
collapse: {
|
||||
0: '<span class="tf-icons bx bx-search bx-18px me-2"></span>Filters',
|
||||
_: '<span class="tf-icons bx bx-search bx-18px me-2"></span>Filters (%d)',
|
||||
},
|
||||
initComplete: function () {
|
||||
const $wrapper = $("#services_wrapper");
|
||||
$wrapper.find(".btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly) {
|
||||
$wrapper
|
||||
.find(".dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in read-only mode; you cannot create new services.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
}
|
||||
},
|
||||
},
|
||||
initComplete: function () {
|
||||
const $wrapper = $("#services_wrapper");
|
||||
$wrapper.find(".btn-secondary").removeClass("btn-secondary");
|
||||
if (isReadOnly) {
|
||||
$wrapper
|
||||
.find(".dt-buttons")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"The database is in read-only mode; you cannot create new services.",
|
||||
)
|
||||
.attr("data-bs-placement", "right")
|
||||
.tooltip();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
services_table.searchPanes.container().hide();
|
||||
|
||||
$(".action-button")
|
||||
.parent()
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
|
||||
$("#services").removeClass("d-none");
|
||||
$("#services-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
};
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-services-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
services_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
services_table.responsive.recalc();
|
||||
|
||||
services_table.on("mouseenter", "td", function () {
|
||||
if (services_table.cell(this).index() === undefined) return;
|
||||
const rowIdx = services_table.cell(this).index().row;
|
||||
|
||||
services_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
|
||||
services_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each(function (el) {
|
||||
if (services_table.cell(el).index().row === rowIdx)
|
||||
el.classList.add("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
services_table.on("mouseleave", "td", function () {
|
||||
services_table
|
||||
.cells()
|
||||
.nodes()
|
||||
.each((el) => el.classList.remove("highlight"));
|
||||
});
|
||||
|
||||
services_table.on("select", function (e, dt, type, indexes) {
|
||||
// Enable the actions button
|
||||
$(".action-button")
|
||||
.removeClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", null)
|
||||
.attr("data-bs-original-title", null)
|
||||
.attr("data-bs-placement", null)
|
||||
.tooltip("dispose");
|
||||
});
|
||||
|
||||
services_table.on("deselect", function (e, dt, type, indexes) {
|
||||
// If no rows are selected, disable the actions button
|
||||
if (services_table.rows({ selected: true }).count() === 0) {
|
||||
$(".action-button")
|
||||
.addClass("disabled")
|
||||
.parent()
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.attr(
|
||||
"data-bs-original-title",
|
||||
"Please select one or more rows to perform an action.",
|
||||
)
|
||||
.attr("data-bs-placement", "top")
|
||||
.tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
services_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
if (column === 0 || column === 1 || column === 7) return;
|
||||
columnVisibility[column] = state;
|
||||
// Check if columVisibility is equal to defaultColsVisibility
|
||||
const isDefault =
|
||||
JSON.stringify(columnVisibility) ===
|
||||
JSON.stringify(defaultColsVisibility);
|
||||
// If it is, remove the key from localStorage
|
||||
if (isDefault) {
|
||||
localStorage.removeItem("bw-services-columns");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"bw-services-columns",
|
||||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$(document).on("click", ".delete-service", function () {
|
||||
if (isReadOnly) {
|
||||
alert("This action is not allowed in read-only mode.");
|
||||
|
|
|
|||
|
|
@ -86,21 +86,6 @@ $(document).ready(() => {
|
|||
.toggleClass("is-invalid", !isValid);
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounce function to limit the rate at which a function can fire.
|
||||
* @param {Function} func - The function to debounce.
|
||||
* @param {number} delay - The delay in milliseconds.
|
||||
* @returns {Function} Debounced function.
|
||||
*/
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return function (...args) {
|
||||
const context = this;
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(context, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts and encodes the server name from the input.
|
||||
* @returns {string} Encoded server name.
|
||||
|
|
|
|||
|
|
@ -414,14 +414,6 @@ $(document).ready(() => {
|
|||
return form;
|
||||
};
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
$("#select-plugin").on("click", () => $pluginSearch.focus());
|
||||
|
||||
$pluginSearch.on(
|
||||
|
|
|
|||
|
|
@ -423,6 +423,28 @@ $(document).ready(() => {
|
|||
updateNotificationsBadge();
|
||||
});
|
||||
|
||||
const saveTheme = debounce((rootUrl, theme) => {
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("theme", theme);
|
||||
data.append("csrf_token", csrfToken);
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
$("#dark-mode-toggle").on("change", function () {
|
||||
// If endpoint is "setup", ignore the theme change
|
||||
if (window.location.pathname.includes("/setup")) return;
|
||||
|
|
@ -463,30 +485,14 @@ $(document).ready(() => {
|
|||
}
|
||||
|
||||
$("#theme").val(darkMode ? "dark" : "light");
|
||||
|
||||
const rootUrl = $(this)
|
||||
.data("root-url")
|
||||
.replace(/\/profile$/, "/set_theme");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
const theme = darkMode ? "dark" : "light";
|
||||
localStorage.setItem("theme", theme); // Save user preference
|
||||
|
||||
const data = new FormData();
|
||||
data.append("theme", theme);
|
||||
data.append("csrf_token", csrfToken);
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
saveTheme(
|
||||
$(this)
|
||||
.data("root-url")
|
||||
.replace(/\/profile$/, "/set_theme"),
|
||||
theme,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,6 +3,10 @@
|
|||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
|
||||
<input type="hidden" id="bans_number" value="{{ bans|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['bans']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{% set current_endpoint = request.path.split("/")[-1] %}
|
||||
{% set theme = theme or "light" %}
|
||||
{% set pro_diamond_url = url_for('static', filename='img/diamond.svg') %}
|
||||
{% set avatar_url = url_for('static', filename='img/avatar_profil_BW.png' if theme == 'light' else 'img/avatar_profil_BW-white.png') %}
|
||||
|
|
@ -125,12 +124,21 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="hidden" id="home-path" value="{{ url_for('home') }}" />
|
||||
<input type="hidden" id="is-read-only" value="{{ is_readonly }}" />
|
||||
<input type="hidden" id="theme" value="{{ theme }}" />
|
||||
<input type="hidden" id="bw-logo" value="{{ url_for('static', filename='img/logo-menu.png') }}" />
|
||||
<input type="hidden" id="bw-logo-white" value="{{ url_for('static', filename='img/logo-menu-white.png') }}" />
|
||||
<input type="hidden" id="avatar-url" value="{{ url_for('static', filename='img/avatar_profil_BW.png') }}" />
|
||||
<input type="hidden" id="avatar-url-white" value="{{ url_for('static', filename='img/avatar_profil_BW-white.png') }}" />
|
||||
<input type="hidden"
|
||||
id="bw-logo"
|
||||
value="{{ url_for('static', filename='img/logo-menu.png') }}" />
|
||||
<input type="hidden"
|
||||
id="bw-logo-white"
|
||||
value="{{ url_for('static', filename='img/logo-menu-white.png') }}" />
|
||||
<input type="hidden"
|
||||
id="avatar-url"
|
||||
value="{{ url_for('static', filename='img/avatar_profil_BW.png') }}" />
|
||||
<input type="hidden"
|
||||
id="avatar-url-white"
|
||||
value="{{ url_for('static', filename='img/avatar_profil_BW-white.png') }}" />
|
||||
<!-- prettier-ignore -->
|
||||
{% if current_endpoint != "loading" %}
|
||||
{% include "flash.html" %}
|
||||
|
|
@ -152,6 +160,8 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='libs/datatables/plugins/ip-address.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/dataTableInit.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% if current_endpoint == "home" or current_endpoint != "plugins" and "plugins" in request.path %}
|
||||
<script src="{{ url_for('static', filename='libs/apexcharts/apexcharts.min.js') }}"
|
||||
|
|
@ -179,6 +189,8 @@
|
|||
<!-- Main JS -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/common.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% if current_endpoint in ("setup", "login", "totp", "loading") %}
|
||||
<script src="{{ url_for('static', filename='js/pages/login.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
value="{{ csrf_token() }}" />
|
||||
<p id="cache-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading cache files...</p>
|
||||
<table id="cache" class="table responsive nowrap position-relative w-100 d-none">
|
||||
<table id="cache"
|
||||
class="table responsive nowrap position-relative w-100 d-none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
id="configs_service_selection"
|
||||
value="{{ config_service }}" />
|
||||
<input type="hidden" id="configs_type_selection" value="{{ config_type }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['configs']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@
|
|||
</div>
|
||||
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
|
||||
<input type="hidden" id="instances_number" value="{{ instances|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['instances']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="job_number" value="{{ jobs|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['jobs']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
<div id="banner-container">
|
||||
<p id="banner-text" class="mb-0 slide-in">
|
||||
Get the most of BunkerWeb by upgrading to the PRO version. More info and free trial <a class="light-href text-white-80"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=banner#pro">here</a>.
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=banner#pro">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@
|
|||
value="{{ csrf_token() }}" />
|
||||
<i class="menu-icon tf-icons dark-mode-toggle-icon bx bx-{% if theme == "light" %}sun{% else %}moon{% endif %}"></i>
|
||||
<div class="d-flex align-items-center justify-content-center w-100 h-100">
|
||||
<label class="setting-checkbox-label me-2 mb-0"
|
||||
for="dark-mode-toggle">Light</label>
|
||||
<label class="setting-checkbox-label me-2 mb-0" for="dark-mode-toggle">Light</label>
|
||||
<div class="form-check form-switch mb-0"
|
||||
{% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly mode, therefore the theme cannot be changed"{% endif %}>
|
||||
<input id="dark-mode-toggle"
|
||||
|
|
@ -52,8 +51,7 @@
|
|||
{% if theme == "dark" %}checked{% endif %}
|
||||
{% if is_readonly %}disabled{% endif %} />
|
||||
</div>
|
||||
<label class="setting-checkbox-label mb-0"
|
||||
for="dark-mode-toggle">Dark</label>
|
||||
<label class="setting-checkbox-label mb-0" for="dark-mode-toggle">Dark</label>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -66,23 +66,23 @@
|
|||
<!-- /Buttons -->
|
||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||
{% if not is_pro_version %}
|
||||
<li class="nav-item lh-1 me-4">
|
||||
<div class="buy-now courier-prime">
|
||||
<a class="btn btn-responsive btn-buy-now"
|
||||
role="button"
|
||||
aria-pressed="true"
|
||||
href="https://panel.bunkerweb.io/order/bunkerweb-pro?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<span class="me-1 me-md-2 d-flex h-100 justify-content-center align-items-center">
|
||||
<img src="{{ url_for('static', filename='img/diamond-white.svg') }}"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">
|
||||
</span>
|
||||
Upgrade to PRO</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item lh-1 me-4">
|
||||
<div class="buy-now courier-prime">
|
||||
<a class="btn btn-responsive btn-buy-now"
|
||||
role="button"
|
||||
aria-pressed="true"
|
||||
href="https://panel.bunkerweb.io/order/bunkerweb-pro?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<span class="me-1 me-md-2 d-flex h-100 justify-content-center align-items-center">
|
||||
<img src="{{ url_for('static', filename='img/diamond-white.svg') }}"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">
|
||||
</span>
|
||||
Upgrade to PRO</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!-- Stars -->
|
||||
<li class="d-none d-md-inline nav-item lh-1 me-4">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="plugins_number" value="{{ plugins|length }}" />
|
||||
<input type="hidden" id="pro_diamond_url" value="{{ pro_diamond_url }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['plugins']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -181,10 +181,7 @@
|
|||
<div class="row g-3">
|
||||
<div class="col-md-12 form-floating">
|
||||
<select class="form-select" id="theme" name="theme">
|
||||
<option value="light"
|
||||
{% if theme == "light" %}selected{% endif %}>
|
||||
Light
|
||||
</option>
|
||||
<option value="light" {% if theme == "light" %}selected{% endif %}>Light</option>
|
||||
<option value="dark" {% if theme == "dark" %}selected{% endif %}>Dark</option>
|
||||
</select>
|
||||
<label for="theme">Theme</label>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@
|
|||
{% set base_flags_url = url_for('static', filename='img/flags') %}
|
||||
<input type="hidden" id="reports_number" value="{{ reports|length }}" />
|
||||
<input type="hidden" id="base_flags_url" value="{{ base_flags_url }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['reports']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<p id="reports-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading reports...</p>
|
||||
<table id="reports"
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@
|
|||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
|
||||
<input type="hidden" id="services_number" value="{{ services|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['services']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<p id="services-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading services...</p>
|
||||
<table id="services" class="table responsive nowrap position-relative w-100 d-none">
|
||||
<table id="services"
|
||||
class="table responsive nowrap position-relative w-100 d-none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
|
|
|
|||
|
|
@ -459,7 +459,8 @@
|
|||
{% if lets_encrypt_staging == "yes" %}checked{% endif %} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Wildcard certificates are only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-4 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Wildcard certificates are only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-USE_LETS_ENCRYPT_WILDCARD"
|
||||
for="USE_LETS_ENCRYPT_WILDCARD"
|
||||
|
|
@ -491,7 +492,7 @@
|
|||
role="switch"
|
||||
aria-labelledby="label-USE_LETS_ENCRYPT_WILDCARD"
|
||||
{% if lets_encrypt_wildcard == "yes" %}checked{% endif %}
|
||||
{% if lets_encrypt_challenge == 'http' %}disabled{% endif %}/>
|
||||
{% if lets_encrypt_challenge == 'http' %}disabled{% endif %} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 pb-3">
|
||||
|
|
@ -564,7 +565,8 @@
|
|||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS provider is only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-md-6 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS provider is only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-LETS_ENCRYPT_DNS_PROVIDER"
|
||||
for="LETS_ENCRYPT_DNS_PROVIDER"
|
||||
|
|
@ -652,7 +654,8 @@
|
|||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS propagation is only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-md-6 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS propagation is only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-LETS_ENCRYPT_DNS_PROPAGATION"
|
||||
for="LETS_ENCRYPT_DNS_PROPAGATION"
|
||||
|
|
@ -685,7 +688,8 @@
|
|||
pattern="^(default|\d+)$"
|
||||
{% if lets_encrypt_challenge == 'http' %}disabled{% endif %} />
|
||||
</div>
|
||||
<div class="col-12 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Credentials are only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-12 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Credentials are only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-LETS_ENCRYPT_DNS_CREDENTIAL_ITEMS"
|
||||
for="LETS_ENCRYPT_DNS_CREDENTIAL_ITEMS"
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% block page %}
|
||||
<!-- Content -->
|
||||
<!-- Dark Mode Toggle - Enhanced Floating Button -->
|
||||
<!-- Dark Mode Toggle - Enhanced Floating Button -->
|
||||
<div class="theme-toggle position-fixed top-0 end-0 p-6"
|
||||
style="z-index: 1030">
|
||||
<div class="toggle-container d-flex align-items-center bg-white p-3 rounded-pill shadow-lg">
|
||||
<label class="setting-checkbox-label pe-2 mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Light</label>
|
||||
<div class="form-switch">
|
||||
<input id="dark-mode-toggle"
|
||||
name="dark-mode-toggle"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
{% if theme == "dark" %}checked{% endif %} />
|
||||
</div>
|
||||
<label class="setting-checkbox-label mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Dark</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Dark Mode Toggle -->
|
||||
style="z-index: 1030">
|
||||
<div class="toggle-container d-flex align-items-center bg-white p-3 rounded-pill shadow-lg">
|
||||
<label class="setting-checkbox-label pe-2 mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Light</label>
|
||||
<div class="form-switch">
|
||||
<input id="dark-mode-toggle"
|
||||
name="dark-mode-toggle"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
{% if theme == "dark" %}checked{% endif %} />
|
||||
</div>
|
||||
<label class="setting-checkbox-label mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Dark</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Dark Mode Toggle -->
|
||||
<div class="bg-{% if theme == 'light' %}light{% else %}dark{% endif %}-subtle">
|
||||
<div class="login-background">
|
||||
<div class="container-xxl">
|
||||
|
|
|
|||
|
|
@ -23,6 +23,63 @@ LOGGER = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO
|
|||
USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ -~]).{8,}$")
|
||||
PLUGIN_NAME_RX = re_compile(r"^[\w.-]{4,64}$")
|
||||
|
||||
COLUMNS_PREFERENCES_DEFAULTS = {
|
||||
"bans": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
},
|
||||
"configs": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": False,
|
||||
},
|
||||
"instances": {
|
||||
"3": False,
|
||||
"4": False,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
"8": True,
|
||||
},
|
||||
"jobs": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
},
|
||||
"plugins": {
|
||||
"2": False,
|
||||
"4": False,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
"8": True,
|
||||
},
|
||||
"reports": {
|
||||
"3": True,
|
||||
"4": False,
|
||||
"5": False,
|
||||
"6": False,
|
||||
"7": False,
|
||||
"8": True,
|
||||
"9": True,
|
||||
"10": False,
|
||||
"11": True,
|
||||
},
|
||||
"services": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def stop_gunicorn():
|
||||
p = Popen(["pgrep", "-f", "gunicorn"], stdout=PIPE)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
from json import dumps
|
||||
from json import dumps, loads
|
||||
from os import getenv, sep
|
||||
from os.path import join
|
||||
from secrets import token_urlsafe
|
||||
|
|
@ -45,6 +45,7 @@ from app.routes.totp import totp
|
|||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.models.models import AnonymousUser
|
||||
from app.utils import (
|
||||
COLUMNS_PREFERENCES_DEFAULTS,
|
||||
TMP_DIR,
|
||||
LOGGER,
|
||||
flash,
|
||||
|
|
@ -139,8 +140,9 @@ with app.app_context():
|
|||
|
||||
@app.context_processor
|
||||
def inject_variables():
|
||||
current_endpoint = request.path.split("/")[-1]
|
||||
if request.path.startswith(("/check_reloading", "/setup", "/loading", "/login", "/totp")):
|
||||
return dict(script_nonce=app.config["SCRIPT_NONCE"])
|
||||
return dict(current_endpoint=current_endpoint, script_nonce=app.config["SCRIPT_NONCE"])
|
||||
|
||||
DATA.load_from_file()
|
||||
metadata = DB.get_metadata()
|
||||
|
|
@ -164,7 +166,8 @@ def inject_variables():
|
|||
flash("The last changes have been applied successfully.", "success")
|
||||
DATA["CONFIG_CHANGED"] = False
|
||||
|
||||
return dict(
|
||||
data = dict(
|
||||
current_endpoint=current_endpoint,
|
||||
script_nonce=app.config["SCRIPT_NONCE"],
|
||||
bw_version=metadata["version"],
|
||||
latest_version=DATA.get("LATEST_VERSION", "unknown"),
|
||||
|
|
@ -177,8 +180,14 @@ def inject_variables():
|
|||
flash_messages=session.get("flash_messages", []),
|
||||
is_readonly=DATA.get("READONLY_MODE", False),
|
||||
theme=current_user.theme if current_user.is_authenticated else "dark",
|
||||
columns_preferences_defaults=COLUMNS_PREFERENCES_DEFAULTS,
|
||||
)
|
||||
|
||||
if current_endpoint in COLUMNS_PREFERENCES_DEFAULTS:
|
||||
data["columns_preferences"] = DB.get_ui_user_columns_preferences(current_user.get_id(), current_endpoint)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(username):
|
||||
|
|
@ -447,7 +456,7 @@ def check_reloading():
|
|||
@login_required
|
||||
def set_theme():
|
||||
if DB.readonly or request.form["theme"] not in ("dark", "light"):
|
||||
return
|
||||
return Response(status=400, response=dumps({"message": "Bad request"}), content_type="application/json")
|
||||
|
||||
user_data = {
|
||||
"username": current_user.get_id(),
|
||||
|
|
@ -461,6 +470,36 @@ def set_theme():
|
|||
ret = DB.update_ui_user(**user_data, old_username=current_user.get_id())
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't update the user {current_user.get_id()}: {ret}")
|
||||
return Response(status=500, response=dumps({"message": "Internal server error"}), content_type="application/json")
|
||||
|
||||
return Response(status=200, response=dumps({"message": "ok"}), content_type="application/json")
|
||||
|
||||
|
||||
@app.route("/set_columns_preferences", methods=["POST"])
|
||||
@login_required
|
||||
def set_columns_preferences():
|
||||
table_name = request.form.get("table_name")
|
||||
columns_preferences = request.form.get("columns_preferences", "{}")
|
||||
|
||||
try:
|
||||
columns_preferences = loads(columns_preferences)
|
||||
except BaseException:
|
||||
return Response(status=400, response=dumps({"message": "Bad request"}), content_type="application/json")
|
||||
|
||||
LOGGER.debug(f"Setting columns preferences for {table_name}: {columns_preferences}")
|
||||
LOGGER.debug(f"Default columns preferences for {table_name}: {COLUMNS_PREFERENCES_DEFAULTS.get(table_name, {})}")
|
||||
|
||||
if (
|
||||
DB.readonly
|
||||
or table_name not in COLUMNS_PREFERENCES_DEFAULTS
|
||||
or any(column not in COLUMNS_PREFERENCES_DEFAULTS[table_name] for column in columns_preferences)
|
||||
):
|
||||
return Response(status=400, response=dumps({"message": "Bad request"}), content_type="application/json")
|
||||
|
||||
ret = DB.update_ui_user_columns_preferences(current_user.get_id(), table_name, columns_preferences)
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't update the user {current_user.get_id()}'s columns preferences: {ret}")
|
||||
return Response(status=500, response=dumps({"message": "Internal server error"}), content_type="application/json")
|
||||
|
||||
return Response(status=200, response=dumps({"message": "ok"}), content_type="application/json")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue