Merge branch 'dev' of github.com:bunkerity/bunkerweb into dev

This commit is contained in:
Florian 2024-11-21 20:13:42 +01:00
commit cf8671655c
No known key found for this signature in database
GPG key ID: 52072123690D7318
36 changed files with 1571 additions and 1951 deletions

View file

@ -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")

View file

@ -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

View 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);
};
}

View 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;
}

View file

@ -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 () {

View file

@ -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),
);
}
});
});

View file

@ -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);

View file

@ -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">&nbsp;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.");

View file

@ -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>&nbsp;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>&nbsp;Up',
value: function (rowData, rowIdx) {
return rowData[5].includes("Up");
},
},
},
{
label:
'<i class="bx bx-xs bx-down-arrow-alt text-danger"></i>&nbsp;Down',
value: function (rowData, rowIdx) {
return rowData[5].includes("Down");
{
label:
'<i class="bx bx-xs bx-down-arrow-alt text-danger"></i>&nbsp;Down',
value: function (rowData, rowIdx) {
return rowData[5].includes("Down");
},
},
},
{
label:
'<i class="bx bx-xs bxs-hourglass text-warning"></i>&nbsp;Loading',
value: function (rowData, rowIdx) {
return rowData[5].includes("Loading");
{
label:
'<i class="bx bx-xs bxs-hourglass text-warning"></i>&nbsp;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>&nbsp;Static',
value: function (rowData, rowIdx) {
return rowData[6].includes("Static");
{
searchPanes: {
show: true,
options: [
{
label: '<i class="bx bx-xs bx-microchip"></i>&nbsp;Static',
value: function (rowData, rowIdx) {
return rowData[6].includes("Static");
},
},
},
{
label: '<i class="bx bx-xs bxl-docker"></i>&nbsp;Container',
value: function (rowData, rowIdx) {
return rowData[6].includes("Container");
{
label: '<i class="bx bx-xs bxl-docker"></i>&nbsp;Container',
value: function (rowData, rowIdx) {
return rowData[6].includes("Container");
},
},
},
{
label: '<i class="bx bx-xs bxl-kubernetes"></i>&nbsp;Pod',
value: function (rowData, rowIdx) {
return rowData[6].includes("Pod");
{
label: '<i class="bx bx-xs bxl-kubernetes"></i>&nbsp;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;

View file

@ -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>&nbsp;No',
value: function (rowData, rowIdx) {
return rowData[5].includes("bx-x");
},
},
{
label: '<i class="bx bx-xs bx-check text-success"></i>&nbsp;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>&nbsp;No',
value: function (rowData, rowIdx) {
return rowData[6].includes("bx-x");
},
},
{
label: '<i class="bx bx-xs bx-check text-success"></i>&nbsp;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>&nbsp;Failed',
value: function (rowData, rowIdx) {
return rowData[7].includes("bx-x");
},
},
{
label:
'<i class="bx bx-xs bx-check text-success"></i>&nbsp;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>&nbsp;No',
value: function (rowData, rowIdx) {
return rowData[5].includes("bx-x");
},
},
{
label:
'<i class="bx bx-xs bx-check text-success"></i>&nbsp;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>&nbsp;No',
value: function (rowData, rowIdx) {
return rowData[6].includes("bx-x");
},
},
{
label:
'<i class="bx bx-xs bx-check text-success"></i>&nbsp;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>&nbsp;Failed',
value: function (rowData, rowIdx) {
return rowData[7].includes("bx-x");
},
},
{
label:
'<i class="bx bx-xs bx-check text-success"></i>&nbsp;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 () {

View file

@ -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");

View file

@ -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">&nbsp;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>&nbsp;No',
value: function (rowData, rowIdx) {
return rowData[6].includes("bx-x");
},
},
{
label: '<i class="bx bx-xs bx-check text-success"></i>&nbsp;Yes',
value: function (rowData, rowIdx) {
return rowData[6].includes("bx-check");
},
},
{
label:
'<i class="bx bx-xs bx-minus text-warning"></i>&nbsp;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">&nbsp;PRO`,
value: function (rowData, rowIdx) {
return rowData[7].includes("PRO");
},
},
{
label: '<i class="bx bx-plug bx-xs"></i>&nbsp;External',
value: function (rowData, rowIdx) {
return rowData[7].includes("EXTERNAL");
},
},
{
label: '<i class="bx bx-cloud-upload bx-xs"></i>&nbsp;UI',
value: function (rowData, rowIdx) {
return rowData[7].includes("UI");
},
},
{
label: '<i class="bx bx-shield bx-xs"></i>&nbsp;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>&nbsp;No',
value: function (rowData, rowIdx) {
return rowData[6].includes("bx-x");
},
},
{
label:
'<i class="bx bx-xs bx-check text-success"></i>&nbsp;Yes',
value: function (rowData, rowIdx) {
return rowData[6].includes("bx-check");
},
},
{
label:
'<i class="bx bx-xs bx-minus text-warning"></i>&nbsp;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">&nbsp;PRO`,
value: function (rowData, rowIdx) {
return rowData[7].includes("PRO");
},
},
{
label: '<i class="bx bx-plug bx-xs"></i>&nbsp;External',
value: function (rowData, rowIdx) {
return rowData[7].includes("EXTERNAL");
},
},
{
label: '<i class="bx bx-cloud-upload bx-xs"></i>&nbsp;UI',
value: function (rowData, rowIdx) {
return rowData[7].includes("UI");
},
},
{
label: '<i class="bx bx-shield bx-xs"></i>&nbsp;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.");

View file

@ -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("#", ""));

View file

@ -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">&nbsp;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>&nbsp;Online',
value: (rowData) => rowData[3].includes("Online"),
},
{
label: '<i class="bx bx-xs bx-file-blank"></i>&nbsp;Draft',
value: (rowData) => rowData[3].includes("Draft"),
},
],
combiner: "or",
orderable: false,
{
searchPanes: {
show: true,
options: [
{
label: '<i class="bx bx-xs bx-globe"></i>&nbsp;Online',
value: (rowData) => rowData[3].includes("Online"),
},
{
label: '<i class="bx bx-xs bx-file-blank"></i>&nbsp;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.");

View file

@ -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.

View file

@ -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(

View file

@ -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

View file

@ -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"

View file

@ -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>

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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"

View file

@ -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>

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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">

View file

@ -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)

View file

@ -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")