mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
feat: add persistent column preferences in database
This commit is contained in:
parent
313edb4df3
commit
cb62f550ec
28 changed files with 696 additions and 175 deletions
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from sqlalchemy import TEXT, Boolean, Column, DateTime, Enum, ForeignKey, Identity, Integer, LargeBinary, String, UnicodeText
|
||||
from json import dumps, loads
|
||||
from typing import Any, Optional
|
||||
from sqlalchemy import TEXT, Boolean, Column, DateTime, Enum, ForeignKey, Identity, Integer, LargeBinary, String, Text, TypeDecorator, UnicodeText
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
|
||||
|
|
@ -310,6 +312,36 @@ class Metadata(Base):
|
|||
## UI Models
|
||||
|
||||
THEMES_ENUM = Enum("light", "dark", name="themes_enum")
|
||||
TABLES_ENUM = Enum("bans", "configs", "instances", "jobs", "plugins", "reports", "services", name="tables_enum")
|
||||
|
||||
|
||||
class JSONText(TypeDecorator):
|
||||
"""
|
||||
Custom JSON type to serialize/deserialize dictionaries as strings.
|
||||
Compatible with all databases (MariaDB, MySQL, PostgreSQL, SQLite).
|
||||
Ensures JSON strings are sorted by keys for consistent storage.
|
||||
"""
|
||||
|
||||
impl = Text # Stores JSON as a TEXT field in the database
|
||||
|
||||
def process_bind_param(self, value: Optional[dict], dialect: Any) -> Optional[str]:
|
||||
"""
|
||||
Convert a dictionary to a JSON string before saving to the database.
|
||||
Sorts dictionary keys for consistent serialization.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
# Serialize dictionary to a sorted JSON string
|
||||
return dumps(dict(sorted(value.items())))
|
||||
|
||||
def process_result_value(self, value: Optional[str], dialect: Any) -> Optional[dict]:
|
||||
"""
|
||||
Convert a JSON string back to a dictionary after retrieving from the database.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
# Deserialize JSON string to dictionary
|
||||
return loads(value)
|
||||
|
||||
|
||||
class Users(Base):
|
||||
|
|
@ -331,6 +363,7 @@ class Users(Base):
|
|||
roles = relationship("RolesUsers", back_populates="user", cascade="all")
|
||||
recovery_codes = relationship("UserRecoveryCodes", back_populates="user", cascade="all")
|
||||
sessions = relationship("UserSessions", back_populates="user", cascade="all")
|
||||
columns_preferences = relationship("UserColumnsPreferences", back_populates="user", cascade="all")
|
||||
list_roles: list[str] = []
|
||||
list_permissions: list[str] = []
|
||||
list_recovery_codes: list[str] = []
|
||||
|
|
@ -396,3 +429,15 @@ class UserSessions(Base):
|
|||
last_activity = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
user = relationship("Users", back_populates="sessions")
|
||||
|
||||
|
||||
class UserColumnsPreferences(Base):
|
||||
__tablename__ = "bw_ui_user_columns_preferences"
|
||||
__table_args__ = (UniqueConstraint("user_name", "table_name"),)
|
||||
|
||||
id = Column(Integer, Identity(start=1, increment=1), primary_key=True)
|
||||
user_name = Column(String(256), ForeignKey("bw_ui_users.username", onupdate="cascade", ondelete="cascade"), nullable=False)
|
||||
table_name = Column(TABLES_ENUM, nullable=False)
|
||||
columns = Column(JSONText, nullable=False)
|
||||
|
||||
user = relationship("Users", back_populates="columns_preferences")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from logging import Logger
|
|||
from os import sep
|
||||
from os.path import join
|
||||
from sys import path as sys_path
|
||||
from typing import List, Literal, Optional, Union
|
||||
from typing import Dict, List, Literal, Optional, Union
|
||||
|
||||
|
||||
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("api",), ("db",))]:
|
||||
|
|
@ -14,9 +14,10 @@ from bcrypt import gensalt, hashpw
|
|||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from Database import Database # type: ignore
|
||||
from model import Permissions, Roles, RolesPermissions, RolesUsers, UserRecoveryCodes, UserSessions # type: ignore
|
||||
from model import Permissions, Roles, RolesPermissions, RolesUsers, UserColumnsPreferences, UserRecoveryCodes, UserSessions # type: ignore
|
||||
|
||||
from app.models.models import UiUsers
|
||||
from app.utils import COLUMNS_PREFERENCES_DEFAULTS
|
||||
|
||||
|
||||
class UIDatabase(Database):
|
||||
|
|
@ -30,13 +31,20 @@ class UIDatabase(Database):
|
|||
query = session.query(UiUsers).filter_by(username=username)
|
||||
else:
|
||||
query = session.query(UiUsers).filter_by(admin=True)
|
||||
query = query.options(joinedload(UiUsers.roles), joinedload(UiUsers.recovery_codes))
|
||||
query = query.options(joinedload(UiUsers.roles), joinedload(UiUsers.recovery_codes), joinedload(UiUsers.columns_preferences))
|
||||
|
||||
ui_user = query.first()
|
||||
|
||||
if not ui_user:
|
||||
return None
|
||||
elif not as_dict:
|
||||
|
||||
if not ui_user.columns_preferences and not self.readonly:
|
||||
for table_name, columns in COLUMNS_PREFERENCES_DEFAULTS.items():
|
||||
session.add(UserColumnsPreferences(user_name=ui_user.username, table_name=table_name, columns=columns))
|
||||
session.commit()
|
||||
session.refresh(ui_user)
|
||||
|
||||
if not as_dict:
|
||||
return ui_user
|
||||
|
||||
ui_user_data = {
|
||||
|
|
@ -102,6 +110,9 @@ class UIDatabase(Database):
|
|||
for code in totp_recovery_codes or []:
|
||||
session.add(UserRecoveryCodes(user_name=username, code=hashpw(code.encode("utf-8"), gensalt(rounds=10)).decode("utf-8")))
|
||||
|
||||
for table_name, columns in COLUMNS_PREFERENCES_DEFAULTS.items():
|
||||
session.add(UserColumnsPreferences(user_name=username, table_name=table_name, columns=columns))
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
|
|
@ -141,6 +152,7 @@ class UIDatabase(Database):
|
|||
session.query(RolesUsers).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserRecoveryCodes).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserSessions).filter_by(user_name=old_username).update({"user_name": username})
|
||||
session.query(UserColumnsPreferences).filter_by(user_name=old_username).update({"user_name": username})
|
||||
|
||||
totp_changed = user.totp_secret != totp_secret
|
||||
|
||||
|
|
@ -176,6 +188,7 @@ class UIDatabase(Database):
|
|||
|
||||
session.query(RolesUsers).filter_by(user_name=username).delete()
|
||||
session.query(UserRecoveryCodes).filter_by(user_name=username).delete()
|
||||
session.query(UserColumnsPreferences).filter_by(user_name=username).delete()
|
||||
session.delete(user)
|
||||
|
||||
try:
|
||||
|
|
@ -417,3 +430,35 @@ class UIDatabase(Database):
|
|||
return str(e)
|
||||
|
||||
return ""
|
||||
|
||||
def update_ui_user_columns_preferences(self, username: str, table_name: str, columns: Dict[str, bool]) -> str:
|
||||
"""Update ui user columns preferences."""
|
||||
with self._db_session() as session:
|
||||
if self.readonly:
|
||||
return "The database is read-only, the changes will not be saved"
|
||||
|
||||
user = session.query(UiUsers).filter_by(username=username).first()
|
||||
if not user:
|
||||
return f"User {username} doesn't exist"
|
||||
|
||||
columns_preferences = session.query(UserColumnsPreferences).filter_by(user_name=username, table_name=table_name).first()
|
||||
if not columns_preferences:
|
||||
return f"Table {table_name} doesn't exist"
|
||||
|
||||
columns_preferences.columns = columns
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
return str(e)
|
||||
|
||||
return ""
|
||||
|
||||
def get_ui_user_columns_preferences(self, username: str, table_name: str) -> Dict[str, bool]:
|
||||
"""Get ui user columns preferences."""
|
||||
with self._db_session() as session:
|
||||
columns_preferences = session.query(UserColumnsPreferences).filter_by(user_name=username, table_name=table_name).first()
|
||||
if not columns_preferences:
|
||||
return COLUMNS_PREFERENCES_DEFAULTS.get(table_name, {})
|
||||
|
||||
return columns_preferences.columns
|
||||
|
|
|
|||
|
|
@ -137,10 +137,16 @@ $(document).ready(function () {
|
|||
};
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let debounceTimer;
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => func.apply(this, args), delay);
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -491,24 +497,21 @@ $(document).ready(function () {
|
|||
$("#bans").removeClass("d-none");
|
||||
$("#bans-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-bans-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
bans_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
bans_table.column(key).visible(value);
|
||||
});
|
||||
|
||||
bans_table.responsive.recalc();
|
||||
|
||||
bans_table.on("mouseenter", "td", function () {
|
||||
|
|
@ -563,6 +566,34 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "bans");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
bans_table.on("column-visibility.dt", function (e, settings, column, state) {
|
||||
if (column < 3 || column === 8) return;
|
||||
columnVisibility[column] = state;
|
||||
|
|
@ -576,6 +607,8 @@ $(document).ready(function () {
|
|||
} else {
|
||||
localStorage.setItem("bw-bans-columns", JSON.stringify(columnVisibility));
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
});
|
||||
|
||||
$(document).on("click", ".unban-ip", function () {
|
||||
|
|
|
|||
|
|
@ -483,24 +483,21 @@ $(document).ready(function () {
|
|||
$("#configs").removeClass("d-none");
|
||||
$("#configs-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
7: false,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-configs-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
configs_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
configs_table.column(key).visible(value);
|
||||
});
|
||||
|
||||
configs_table.responsive.recalc();
|
||||
|
||||
configs_table.on("mouseenter", "td", function () {
|
||||
|
|
@ -555,6 +552,48 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "configs");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
configs_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
|
|
@ -573,6 +612,8 @@ $(document).ready(function () {
|
|||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -600,25 +600,21 @@ $(document).ready(function () {
|
|||
$("#instances").removeClass("d-none");
|
||||
$("#instances-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: false,
|
||||
4: false,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
8: true,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-instances-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
instances_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
instances_table.column(key).visible(value);
|
||||
});
|
||||
|
||||
instances_table.responsive.recalc();
|
||||
|
||||
instances_table.on("mouseenter", "td", function () {
|
||||
|
|
@ -673,6 +669,48 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "instances");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
instances_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
|
|
@ -691,6 +729,8 @@ $(document).ready(function () {
|
|||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -376,24 +376,21 @@ $(document).ready(function () {
|
|||
$("#jobs").removeClass("d-none");
|
||||
$("#jobs-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-jobs-columns");
|
||||
var columnVisibility = localStorage.getItem("bw-instances-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
jobs_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
instances_table.column(key).visible(value);
|
||||
});
|
||||
|
||||
jobs_table.responsive.recalc();
|
||||
|
||||
jobs_table.on("mouseenter", "td", function () {
|
||||
|
|
@ -448,6 +445,48 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "jobs");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
jobs_table.on("column-visibility.dt", function (e, settings, column, state) {
|
||||
if (column < 3 || column === 8) return;
|
||||
columnVisibility[column] = state;
|
||||
|
|
@ -461,6 +500,8 @@ $(document).ready(function () {
|
|||
} else {
|
||||
localStorage.setItem("bw-jobs-columns", JSON.stringify(columnVisibility));
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
});
|
||||
|
||||
$(document).on("click", ".show-history", function () {
|
||||
|
|
|
|||
|
|
@ -475,25 +475,21 @@ $(document).ready(function () {
|
|||
$("#plugins").removeClass("d-none");
|
||||
$("#plugins-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
2: false,
|
||||
4: false,
|
||||
5: true,
|
||||
6: true,
|
||||
7: true,
|
||||
8: true,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-plugins-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
plugins_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
plugins_table.column(key).visible(value);
|
||||
});
|
||||
|
||||
plugins_table.responsive.recalc();
|
||||
|
||||
plugins_table.on("mouseenter", "td", function () {
|
||||
|
|
@ -548,6 +544,48 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "plugins");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
plugins_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
|
|
@ -566,6 +604,8 @@ $(document).ready(function () {
|
|||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -501,33 +501,68 @@ $(function () {
|
|||
$("#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,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-reports-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
reports_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "reports");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
reports_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
|
|
@ -546,6 +581,8 @@ $(function () {
|
|||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -491,23 +491,21 @@ $(function () {
|
|||
$("#services").removeClass("d-none");
|
||||
$("#services-waiting").addClass("visually-hidden");
|
||||
|
||||
const defaultColsVisibility = {
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
};
|
||||
const defaultColsVisibility = JSON.parse(
|
||||
$("#columns_preferences_defaults").val().trim(),
|
||||
);
|
||||
|
||||
var columnVisibility = localStorage.getItem("bw-services-columns");
|
||||
if (columnVisibility === null) {
|
||||
columnVisibility = JSON.parse(JSON.stringify(defaultColsVisibility));
|
||||
columnVisibility = JSON.parse($("#columns_preferences").val().trim());
|
||||
} else {
|
||||
columnVisibility = JSON.parse(columnVisibility);
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
services_table.column(key).visible(value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(columnVisibility).forEach(([key, value]) => {
|
||||
services_table.column(key).visible(value);
|
||||
});
|
||||
|
||||
services_table.responsive.recalc();
|
||||
|
||||
services_table.on("mouseenter", "td", function () {
|
||||
|
|
@ -562,6 +560,48 @@ $(function () {
|
|||
}
|
||||
});
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const saveColumnsPreferences = debounce(() => {
|
||||
const rootUrl = $("#home-path")
|
||||
.val()
|
||||
.trim()
|
||||
.replace(/\/home$/, "/set_columns_preferences");
|
||||
const csrfToken = $("#csrf_token").val();
|
||||
|
||||
const data = new FormData();
|
||||
data.append("csrf_token", csrfToken);
|
||||
data.append("table_name", "services");
|
||||
data.append("columns_preferences", JSON.stringify(columnVisibility));
|
||||
|
||||
fetch(rootUrl, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
console.log("Preferences saved successfully!");
|
||||
// Handle success, redirect, etc.
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
services_table.on(
|
||||
"column-visibility.dt",
|
||||
function (e, settings, column, state) {
|
||||
|
|
@ -580,6 +620,8 @@ $(function () {
|
|||
JSON.stringify(columnVisibility),
|
||||
);
|
||||
}
|
||||
|
||||
saveColumnsPreferences();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -423,6 +423,42 @@ $(document).ready(() => {
|
|||
updateNotificationsBadge();
|
||||
});
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let timer = null;
|
||||
|
||||
return (...args) => {
|
||||
// Clear the timer if the function is called again during the delay
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
// Start a new timer to invoke the function after the delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
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 +499,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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
|
||||
<input type="hidden" id="bans_number" value="{{ bans|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['bans']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{% set current_endpoint = request.path.split("/")[-1] %}
|
||||
{% set theme = theme or "light" %}
|
||||
{% set pro_diamond_url = url_for('static', filename='img/diamond.svg') %}
|
||||
{% set avatar_url = url_for('static', filename='img/avatar_profil_BW.png' if theme == 'light' else 'img/avatar_profil_BW-white.png') %}
|
||||
|
|
@ -125,12 +124,21 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="hidden" id="home-path" value="{{ url_for('home') }}" />
|
||||
<input type="hidden" id="is-read-only" value="{{ is_readonly }}" />
|
||||
<input type="hidden" id="theme" value="{{ theme }}" />
|
||||
<input type="hidden" id="bw-logo" value="{{ url_for('static', filename='img/logo-menu.png') }}" />
|
||||
<input type="hidden" id="bw-logo-white" value="{{ url_for('static', filename='img/logo-menu-white.png') }}" />
|
||||
<input type="hidden" id="avatar-url" value="{{ url_for('static', filename='img/avatar_profil_BW.png') }}" />
|
||||
<input type="hidden" id="avatar-url-white" value="{{ url_for('static', filename='img/avatar_profil_BW-white.png') }}" />
|
||||
<input type="hidden"
|
||||
id="bw-logo"
|
||||
value="{{ url_for('static', filename='img/logo-menu.png') }}" />
|
||||
<input type="hidden"
|
||||
id="bw-logo-white"
|
||||
value="{{ url_for('static', filename='img/logo-menu-white.png') }}" />
|
||||
<input type="hidden"
|
||||
id="avatar-url"
|
||||
value="{{ url_for('static', filename='img/avatar_profil_BW.png') }}" />
|
||||
<input type="hidden"
|
||||
id="avatar-url-white"
|
||||
value="{{ url_for('static', filename='img/avatar_profil_BW-white.png') }}" />
|
||||
<!-- prettier-ignore -->
|
||||
{% if current_endpoint != "loading" %}
|
||||
{% include "flash.html" %}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
value="{{ csrf_token() }}" />
|
||||
<p id="cache-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading cache files...</p>
|
||||
<table id="cache" class="table responsive nowrap position-relative w-100 d-none">
|
||||
<table id="cache"
|
||||
class="table responsive nowrap position-relative w-100 d-none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
id="configs_service_selection"
|
||||
value="{{ config_service }}" />
|
||||
<input type="hidden" id="configs_type_selection" value="{{ config_type }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['configs']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@
|
|||
</div>
|
||||
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
|
||||
<input type="hidden" id="instances_number" value="{{ instances|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['instances']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="job_number" value="{{ jobs|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['jobs']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
<div id="banner-container">
|
||||
<p id="banner-text" class="mb-0 slide-in">
|
||||
Get the most of BunkerWeb by upgrading to the PRO version. More info and free trial <a class="light-href text-white-80"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=banner#pro">here</a>.
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=banner#pro">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@
|
|||
value="{{ csrf_token() }}" />
|
||||
<i class="menu-icon tf-icons dark-mode-toggle-icon bx bx-{% if theme == "light" %}sun{% else %}moon{% endif %}"></i>
|
||||
<div class="d-flex align-items-center justify-content-center w-100 h-100">
|
||||
<label class="setting-checkbox-label me-2 mb-0"
|
||||
for="dark-mode-toggle">Light</label>
|
||||
<label class="setting-checkbox-label me-2 mb-0" for="dark-mode-toggle">Light</label>
|
||||
<div class="form-check form-switch mb-0"
|
||||
{% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly mode, therefore the theme cannot be changed"{% endif %}>
|
||||
<input id="dark-mode-toggle"
|
||||
|
|
@ -52,8 +51,7 @@
|
|||
{% if theme == "dark" %}checked{% endif %}
|
||||
{% if is_readonly %}disabled{% endif %} />
|
||||
</div>
|
||||
<label class="setting-checkbox-label mb-0"
|
||||
for="dark-mode-toggle">Dark</label>
|
||||
<label class="setting-checkbox-label mb-0" for="dark-mode-toggle">Dark</label>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -66,23 +66,23 @@
|
|||
<!-- /Buttons -->
|
||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||
{% if not is_pro_version %}
|
||||
<li class="nav-item lh-1 me-4">
|
||||
<div class="buy-now courier-prime">
|
||||
<a class="btn btn-responsive btn-buy-now"
|
||||
role="button"
|
||||
aria-pressed="true"
|
||||
href="https://panel.bunkerweb.io/order/bunkerweb-pro?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<span class="me-1 me-md-2 d-flex h-100 justify-content-center align-items-center">
|
||||
<img src="{{ url_for('static', filename='img/diamond-white.svg') }}"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">
|
||||
</span>
|
||||
Upgrade to PRO</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item lh-1 me-4">
|
||||
<div class="buy-now courier-prime">
|
||||
<a class="btn btn-responsive btn-buy-now"
|
||||
role="button"
|
||||
aria-pressed="true"
|
||||
href="https://panel.bunkerweb.io/order/bunkerweb-pro?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<span class="me-1 me-md-2 d-flex h-100 justify-content-center align-items-center">
|
||||
<img src="{{ url_for('static', filename='img/diamond-white.svg') }}"
|
||||
alt="Pro plugin"
|
||||
width="18px"
|
||||
height="15.5px">
|
||||
</span>
|
||||
Upgrade to PRO</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!-- Stars -->
|
||||
<li class="d-none d-md-inline nav-item lh-1 me-4">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
<div class="card table-responsive text-nowrap p-4 min-vh-70">
|
||||
<input type="hidden" id="plugins_number" value="{{ plugins|length }}" />
|
||||
<input type="hidden" id="pro_diamond_url" value="{{ pro_diamond_url }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['plugins']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
|
|
|
|||
|
|
@ -181,10 +181,7 @@
|
|||
<div class="row g-3">
|
||||
<div class="col-md-12 form-floating">
|
||||
<select class="form-select" id="theme" name="theme">
|
||||
<option value="light"
|
||||
{% if theme == "light" %}selected{% endif %}>
|
||||
Light
|
||||
</option>
|
||||
<option value="light" {% if theme == "light" %}selected{% endif %}>Light</option>
|
||||
<option value="dark" {% if theme == "dark" %}selected{% endif %}>Dark</option>
|
||||
</select>
|
||||
<label for="theme">Theme</label>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@
|
|||
{% set base_flags_url = url_for('static', filename='img/flags') %}
|
||||
<input type="hidden" id="reports_number" value="{{ reports|length }}" />
|
||||
<input type="hidden" id="base_flags_url" value="{{ base_flags_url }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['reports']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<p id="reports-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading reports...</p>
|
||||
<table id="reports"
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@
|
|||
<!-- Content -->
|
||||
<div class="card table-responsive text-nowrap p-4 pb-8 min-vh-70">
|
||||
<input type="hidden" id="services_number" value="{{ services|length }}" />
|
||||
<textarea type="hidden"
|
||||
id="columns_preferences_defaults"
|
||||
class="visually-hidden">{{ columns_preferences_defaults['services']|tojson }}</textarea>
|
||||
<textarea type="hidden" id="columns_preferences" class="visually-hidden">{{ columns_preferences|tojson }}</textarea>
|
||||
<input type="hidden"
|
||||
id="csrf_token"
|
||||
name="csrf_token"
|
||||
value="{{ csrf_token() }}" />
|
||||
<p id="services-waiting"
|
||||
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading services...</p>
|
||||
<table id="services" class="table responsive nowrap position-relative w-100 d-none">
|
||||
<table id="services"
|
||||
class="table responsive nowrap position-relative w-100 d-none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-bs-toggle="tooltip"
|
||||
|
|
|
|||
|
|
@ -459,7 +459,8 @@
|
|||
{% if lets_encrypt_staging == "yes" %}checked{% endif %} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Wildcard certificates are only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-4 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Wildcard certificates are only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-USE_LETS_ENCRYPT_WILDCARD"
|
||||
for="USE_LETS_ENCRYPT_WILDCARD"
|
||||
|
|
@ -491,7 +492,7 @@
|
|||
role="switch"
|
||||
aria-labelledby="label-USE_LETS_ENCRYPT_WILDCARD"
|
||||
{% if lets_encrypt_wildcard == "yes" %}checked{% endif %}
|
||||
{% if lets_encrypt_challenge == 'http' %}disabled{% endif %}/>
|
||||
{% if lets_encrypt_challenge == 'http' %}disabled{% endif %} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 pb-3">
|
||||
|
|
@ -564,7 +565,8 @@
|
|||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS provider is only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-md-6 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS provider is only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-LETS_ENCRYPT_DNS_PROVIDER"
|
||||
for="LETS_ENCRYPT_DNS_PROVIDER"
|
||||
|
|
@ -652,7 +654,8 @@
|
|||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS propagation is only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-md-6 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="DNS propagation is only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-LETS_ENCRYPT_DNS_PROPAGATION"
|
||||
for="LETS_ENCRYPT_DNS_PROPAGATION"
|
||||
|
|
@ -685,7 +688,8 @@
|
|||
pattern="^(default|\d+)$"
|
||||
{% if lets_encrypt_challenge == 'http' %}disabled{% endif %} />
|
||||
</div>
|
||||
<div class="col-12 pb-3"{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Credentials are only supported with DNS challenges."{% endif %}>
|
||||
<div class="col-12 pb-3"
|
||||
{% if lets_encrypt_challenge == 'http' %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Credentials are only supported with DNS challenges."{% endif %}>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label id="label-LETS_ENCRYPT_DNS_CREDENTIAL_ITEMS"
|
||||
for="LETS_ENCRYPT_DNS_CREDENTIAL_ITEMS"
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% block page %}
|
||||
<!-- Content -->
|
||||
<!-- Dark Mode Toggle - Enhanced Floating Button -->
|
||||
<!-- Dark Mode Toggle - Enhanced Floating Button -->
|
||||
<div class="theme-toggle position-fixed top-0 end-0 p-6"
|
||||
style="z-index: 1030">
|
||||
<div class="toggle-container d-flex align-items-center bg-white p-3 rounded-pill shadow-lg">
|
||||
<label class="setting-checkbox-label pe-2 mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Light</label>
|
||||
<div class="form-switch">
|
||||
<input id="dark-mode-toggle"
|
||||
name="dark-mode-toggle"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
{% if theme == "dark" %}checked{% endif %} />
|
||||
</div>
|
||||
<label class="setting-checkbox-label mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Dark</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Dark Mode Toggle -->
|
||||
style="z-index: 1030">
|
||||
<div class="toggle-container d-flex align-items-center bg-white p-3 rounded-pill shadow-lg">
|
||||
<label class="setting-checkbox-label pe-2 mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Light</label>
|
||||
<div class="form-switch">
|
||||
<input id="dark-mode-toggle"
|
||||
name="dark-mode-toggle"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
{% if theme == "dark" %}checked{% endif %} />
|
||||
</div>
|
||||
<label class="setting-checkbox-label mb-0 fw-bold text-secondary"
|
||||
for="dark-mode-toggle">Dark</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Dark Mode Toggle -->
|
||||
<div class="bg-{% if theme == 'light' %}light{% else %}dark{% endif %}-subtle">
|
||||
<div class="login-background">
|
||||
<div class="container-xxl">
|
||||
|
|
|
|||
|
|
@ -23,6 +23,63 @@ LOGGER = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO
|
|||
USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ -~]).{8,}$")
|
||||
PLUGIN_NAME_RX = re_compile(r"^[\w.-]{4,64}$")
|
||||
|
||||
COLUMNS_PREFERENCES_DEFAULTS = {
|
||||
"bans": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
},
|
||||
"configs": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": False,
|
||||
},
|
||||
"instances": {
|
||||
"3": False,
|
||||
"4": False,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
"8": True,
|
||||
},
|
||||
"jobs": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
},
|
||||
"plugins": {
|
||||
"2": False,
|
||||
"4": False,
|
||||
"5": True,
|
||||
"6": True,
|
||||
"7": True,
|
||||
"8": True,
|
||||
},
|
||||
"reports": {
|
||||
"3": True,
|
||||
"4": False,
|
||||
"5": False,
|
||||
"6": False,
|
||||
"7": False,
|
||||
"8": True,
|
||||
"9": True,
|
||||
"10": False,
|
||||
"11": True,
|
||||
},
|
||||
"services": {
|
||||
"3": True,
|
||||
"4": True,
|
||||
"5": True,
|
||||
"6": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def stop_gunicorn():
|
||||
p = Popen(["pgrep", "-f", "gunicorn"], stdout=PIPE)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
from json import dumps
|
||||
from json import dumps, loads
|
||||
from os import getenv, sep
|
||||
from os.path import join
|
||||
from secrets import token_urlsafe
|
||||
|
|
@ -45,6 +45,7 @@ from app.routes.totp import totp
|
|||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.models.models import AnonymousUser
|
||||
from app.utils import (
|
||||
COLUMNS_PREFERENCES_DEFAULTS,
|
||||
TMP_DIR,
|
||||
LOGGER,
|
||||
flash,
|
||||
|
|
@ -139,8 +140,9 @@ with app.app_context():
|
|||
|
||||
@app.context_processor
|
||||
def inject_variables():
|
||||
current_endpoint = request.path.split("/")[-1]
|
||||
if request.path.startswith(("/check_reloading", "/setup", "/loading", "/login", "/totp")):
|
||||
return dict(script_nonce=app.config["SCRIPT_NONCE"])
|
||||
return dict(current_endpoint=current_endpoint, script_nonce=app.config["SCRIPT_NONCE"])
|
||||
|
||||
DATA.load_from_file()
|
||||
metadata = DB.get_metadata()
|
||||
|
|
@ -164,7 +166,8 @@ def inject_variables():
|
|||
flash("The last changes have been applied successfully.", "success")
|
||||
DATA["CONFIG_CHANGED"] = False
|
||||
|
||||
return dict(
|
||||
data = dict(
|
||||
current_endpoint=current_endpoint,
|
||||
script_nonce=app.config["SCRIPT_NONCE"],
|
||||
bw_version=metadata["version"],
|
||||
latest_version=DATA.get("LATEST_VERSION", "unknown"),
|
||||
|
|
@ -177,8 +180,14 @@ def inject_variables():
|
|||
flash_messages=session.get("flash_messages", []),
|
||||
is_readonly=DATA.get("READONLY_MODE", False),
|
||||
theme=current_user.theme if current_user.is_authenticated else "dark",
|
||||
columns_preferences_defaults=COLUMNS_PREFERENCES_DEFAULTS,
|
||||
)
|
||||
|
||||
if current_endpoint in COLUMNS_PREFERENCES_DEFAULTS:
|
||||
data["columns_preferences"] = DB.get_ui_user_columns_preferences(current_user.get_id(), current_endpoint)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(username):
|
||||
|
|
@ -447,7 +456,7 @@ def check_reloading():
|
|||
@login_required
|
||||
def set_theme():
|
||||
if DB.readonly or request.form["theme"] not in ("dark", "light"):
|
||||
return
|
||||
return Response(status=400, response=dumps({"message": "Bad request"}), content_type="application/json")
|
||||
|
||||
user_data = {
|
||||
"username": current_user.get_id(),
|
||||
|
|
@ -461,6 +470,36 @@ def set_theme():
|
|||
ret = DB.update_ui_user(**user_data, old_username=current_user.get_id())
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't update the user {current_user.get_id()}: {ret}")
|
||||
return Response(status=500, response=dumps({"message": "Internal server error"}), content_type="application/json")
|
||||
|
||||
return Response(status=200, response=dumps({"message": "ok"}), content_type="application/json")
|
||||
|
||||
|
||||
@app.route("/set_columns_preferences", methods=["POST"])
|
||||
@login_required
|
||||
def set_columns_preferences():
|
||||
table_name = request.form.get("table_name")
|
||||
columns_preferences = request.form.get("columns_preferences", "{}")
|
||||
|
||||
try:
|
||||
columns_preferences = loads(columns_preferences)
|
||||
except BaseException:
|
||||
return Response(status=400, response=dumps({"message": "Bad request"}), content_type="application/json")
|
||||
|
||||
LOGGER.debug(f"Setting columns preferences for {table_name}: {columns_preferences}")
|
||||
LOGGER.debug(f"Default columns preferences for {table_name}: {COLUMNS_PREFERENCES_DEFAULTS.get(table_name, {})}")
|
||||
|
||||
if (
|
||||
DB.readonly
|
||||
or table_name not in COLUMNS_PREFERENCES_DEFAULTS
|
||||
or any(column not in COLUMNS_PREFERENCES_DEFAULTS[table_name] for column in columns_preferences)
|
||||
):
|
||||
return Response(status=400, response=dumps({"message": "Bad request"}), content_type="application/json")
|
||||
|
||||
ret = DB.update_ui_user_columns_preferences(current_user.get_id(), table_name, columns_preferences)
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't update the user {current_user.get_id()}'s columns preferences: {ret}")
|
||||
return Response(status=500, response=dumps({"message": "Internal server error"}), content_type="application/json")
|
||||
|
||||
return Response(status=200, response=dumps({"message": "ok"}), content_type="application/json")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue