makes logger much more beautiful

This commit is contained in:
Michael Beck 2025-06-13 12:57:54 +02:00
parent 4a10052eae
commit 24f9eb5766
4 changed files with 189 additions and 85 deletions

View File

@ -15,7 +15,7 @@ def list_logs():
# The actual data loading will be handled by JavaScript via the API endpoint
# Get filter parameters for initial state
category = request.args.get("category")
categories_param = request.args.getlist("category") # Get multiple categories
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
search_term = request.args.get("search_term")
@ -28,7 +28,7 @@ def list_logs():
return render_template(
"logs.html.jinja",
categories=categories,
category=category,
selected_categories=categories_param, # Pass selected categories
start_date=start_date,
end_date=end_date,
search_term=search_term,
@ -39,15 +39,15 @@ def list_logs():
@bp.route("/download")
def download_logs():
# Filters - reuse logic from list_logs
category = request.args.get("category")
categories = request.args.getlist("category") # Get multiple categories
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
search_term = request.args.get("search_term")
query = ActivityLog.query
if category:
query = query.filter(ActivityLog.category == category)
if categories:
query = query.filter(ActivityLog.category.in_(categories))
if start_date:
start_date_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(ActivityLog.timestamp >= start_date_dt)

View File

@ -27,26 +27,27 @@ class LoggerManager {
initElements() {
// Form elements
this.filtersForm = document.getElementById("logFiltersForm");
this.categorySelect = document.getElementById("category");
this.statusSelect = document.getElementById("status");
this.startDateInput = document.getElementById("start_date");
this.endDateInput = document.getElementById("end_date");
this.searchTermInput = document.getElementById("search_term");
this.filtersForm = document.getElementById("filterForm");
this.categoryCheckboxes = document.querySelectorAll(".category-checkbox");
this.selectAllCategories = document.getElementById("selectAllCategories");
this.statusSelect = document.getElementById("statusFilter");
this.startDateInput = document.getElementById("startDate");
this.endDateInput = document.getElementById("endDate");
this.searchTermInput = document.getElementById("searchTerm");
this.clearFiltersBtn = document.getElementById("clearFilters");
this.downloadLogsBtn = document.getElementById("downloadLogs");
this.refreshLogsBtn = document.getElementById("refreshLogs");
// Logs display elements
this.logsTableBody = document.getElementById("logsTableBody");
this.pageSizeSelect = document.getElementById("logPageSize");
this.pageSizeSelect = document.getElementById("pageSize");
// Pagination elements
this.paginationContainer = document.getElementById("logsPagination");
this.paginationInfo = document.getElementById("logsPaginationInfo");
this.prevPageBtn = document.getElementById("logsPrevPage");
this.nextPageBtn = document.getElementById("logsNextPage");
this.currentPageSpan = document.getElementById("logsCurrentPage");
this.paginationInfo = document.getElementById("paginationDetails");
this.prevPageBtn = document.getElementById("prevPage");
this.nextPageBtn = document.getElementById("nextPage");
this.currentPageSpan = document.getElementById("currentPageSpan");
// Modal
this.logModal = new ModalHandler("logDetailModal", "log-detail-content");
@ -61,20 +62,37 @@ class LoggerManager {
});
}
// Individual filter changes for immediate application
[
this.categorySelect,
this.statusSelect,
this.startDateInput,
this.endDateInput,
].forEach((element) => {
if (element) {
element.addEventListener("change", () => {
this.applyFilters();
// Handle "Select All" checkbox for categories
if (this.selectAllCategories) {
this.selectAllCategories.addEventListener("change", () => {
const isChecked = this.selectAllCategories.checked;
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
});
}
this.applyFilters();
});
}
// Handle individual category checkboxes
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
// Update "Select All" checkbox state
this.updateSelectAllState();
this.applyFilters();
});
});
// Individual filter changes for immediate application
[this.statusSelect, this.startDateInput, this.endDateInput].forEach(
(element) => {
if (element) {
element.addEventListener("change", () => {
this.applyFilters();
});
}
}
);
// Search term with debounce
if (this.searchTermInput) {
let searchTimeout;
@ -139,11 +157,43 @@ class LoggerManager {
}
}
applyInitialFilters() {
// Set form values from initial filters
if (this.categorySelect && this.initialFilters.category) {
this.categorySelect.value = this.initialFilters.category;
updateSelectAllState() {
const checkedCount = Array.from(this.categoryCheckboxes).filter(
(cb) => cb.checked
).length;
const totalCount = this.categoryCheckboxes.length;
if (checkedCount === 0) {
this.selectAllCategories.checked = false;
this.selectAllCategories.indeterminate = false;
} else if (checkedCount === totalCount) {
this.selectAllCategories.checked = true;
this.selectAllCategories.indeterminate = false;
} else {
this.selectAllCategories.checked = false;
this.selectAllCategories.indeterminate = true;
}
}
getSelectedCategories() {
return Array.from(this.categoryCheckboxes)
.filter((checkbox) => checkbox.checked)
.map((checkbox) => checkbox.value);
}
applyInitialFilters() {
// Set category checkboxes from initial filters
if (this.initialFilters.category) {
const selectedCategories = Array.isArray(this.initialFilters.category)
? this.initialFilters.category
: [this.initialFilters.category];
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.checked = selectedCategories.includes(checkbox.value);
});
this.updateSelectAllState();
}
if (this.startDateInput && this.initialFilters.start_date) {
this.startDateInput.value = this.initialFilters.start_date;
}
@ -157,8 +207,10 @@ class LoggerManager {
applyFilters() {
// Collect current filter values
const selectedCategories = this.getSelectedCategories();
this.filters = {
category: this.categorySelect?.value || "",
category: selectedCategories, // Now an array
status: this.statusSelect?.value || "",
start_date: this.startDateInput?.value || "",
end_date: this.endDateInput?.value || "",
@ -176,8 +228,15 @@ class LoggerManager {
}
clearAllFilters() {
// Clear all form fields
if (this.categorySelect) this.categorySelect.value = "";
// Clear all category checkboxes and select all
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.checked = true; // Default to all selected
});
if (this.selectAllCategories) {
this.selectAllCategories.checked = true;
this.selectAllCategories.indeterminate = false;
}
if (this.statusSelect) this.statusSelect.value = "";
if (this.startDateInput) this.startDateInput.value = "";
if (this.endDateInput) this.endDateInput.value = "";
@ -193,7 +252,7 @@ class LoggerManager {
try {
// Show loading state
this.logsTableBody.innerHTML =
'<tr><td colspan="5" class="text-center">Loading logs...</td></tr>';
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div> Loading logs...</td></tr>';
// Build query parameters
const params = new URLSearchParams({
@ -204,7 +263,14 @@ class LoggerManager {
// Add filters to query
Object.entries(this.filters).forEach(([key, value]) => {
if (value) {
params.append(key, value);
if (key === "category" && Array.isArray(value)) {
// Handle multiple categories
value.forEach((cat) => {
if (cat) params.append("category", cat);
});
} else if (value) {
params.append(key, value);
}
}
});
@ -239,7 +305,7 @@ class LoggerManager {
logs.forEach((log) => {
const row = document.createElement("tr");
row.className = "log-item";
row.className = "log-entry";
row.setAttribute("data-log-id", log.id);
// Format timestamp
@ -259,7 +325,7 @@ class LoggerManager {
<td>${log.description || ""}</td>
`;
// Add click handler for details modal
// Add click handler for details modal - whole row is clickable
row.addEventListener("click", () => {
const url = `/logs/${log.id}/detail`;
this.logModal.loadAndShow(url, "Error loading log details.");
@ -360,7 +426,14 @@ class LoggerManager {
Object.entries(this.filters).forEach(([key, value]) => {
if (value) {
params.append(key, value);
if (key === "category" && Array.isArray(value)) {
// Handle multiple categories
value.forEach((cat) => {
if (cat) params.append("category", cat);
});
} else if (value) {
params.append(key, value);
}
}
});
@ -376,7 +449,14 @@ class LoggerManager {
Object.entries(this.filters).forEach(([key, value]) => {
if (value) {
params.append(key, value);
if (key === "category" && Array.isArray(value)) {
// Handle multiple categories
value.forEach((cat) => {
if (cat) params.append("category", cat);
});
} else if (value) {
params.append(key, value);
}
}
});

View File

@ -7,6 +7,7 @@
<meta name="keywords" content="science, papers, research, management" />
<title>{{ app_title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Optional Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@ -41,6 +41,15 @@
font-weight: 600;
}
.log-entry {
cursor: pointer;
transition: background-color 0.2s ease;
}
.log-entry:hover {
background-color: #f8f9fa;
}
.pagination-info {
font-size: 0.875rem;
color: #6c757d;
@ -70,7 +79,7 @@
{% block content %}
<div class="container-fluid mt-4">
<h1><i class="fas fa-list-alt"></i> Activity Logs</h1>
<h1><i class="bi bi-list-ul"></i> Activity Logs</h1>
<!-- Include standardized flash messages -->
{% include "partials/flash_messages.html.jinja" %}
@ -79,56 +88,66 @@
<!-- Filter Panel -->
<div class="filter-panel">
<form id="filterForm" class="row g-3">
<div class="col-md-2">
<label for="categoryFilter" class="form-label">Category:</label>
<select id="categoryFilter" class="form-select form-select-sm">
<option value="">All Categories</option>
<div class="col-md-3">
<label class="form-label">Categories:</label>
<div class="category-checkbox-container p-2"
style="max-height: 200px; overflow-y: auto; background-color: white; border: 1px solid #ced4da; border-radius: 0.375rem;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllCategories" {% if not
selected_categories or selected_categories|length==categories|length %}checked{% endif
%}>
<label class="form-check-label fw-bold" for="selectAllCategories">
All Categories
</label>
</div>
<hr class="my-2">
{% for cat in categories %}
<option value="{{ cat }}" {% if category==cat %}selected{% endif %}>{{ cat.replace('_', '
').title() }}</option>
<div class="form-check">
<input class="form-check-input category-checkbox" type="checkbox" id="category_{{ cat }}"
value="{{ cat }}" {% if not selected_categories or cat in selected_categories
%}checked{% endif %}>
<label class="form-check-label" for="category_{{ cat }}">
{{ cat.replace('_', ' ').title() }}
</label>
</div>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-2">
<label for="statusFilter" class="form-label">Status:</label>
<select id="statusFilter" class="form-select form-select-sm">
<option value="">All Statuses</option>
<option value="success">Success</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
<option value="pending">Pending</option>
</select>
<div class="col-md-3">
<div class="row">
<label for="statusFilter" class="form-label">Status:</label>
<select id="statusFilter" class="form-select form-select-sm">
<option value="">All Statuses</option>
<option value="success">Success</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
<option value="pending">Pending</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="col-md-3">
<label for="startDate" class="form-label">Start Date:</label>
<input type="date" id="startDate" class="form-control form-control-sm"
value="{{ start_date or '' }}">
</div>
<div class="col-md-2">
<label for="endDate" class="form-label">End Date:</label>
<label for="endDate" class="form-label mt-2">End Date:</label>
<input type="date" id="endDate" class="form-control form-control-sm" value="{{ end_date or '' }}">
</div>
<div class="col-md-3">
<label for="searchTerm" class="form-label">Search:</label>
<input type="text" id="searchTerm" class="form-control form-control-sm"
placeholder="Search action or description..." value="{{ search_term or '' }}">
placeholder="Search in actions and descriptions" value="{{ search_term or '' }}">
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="button" id="applyFilters" class="btn btn-primary btn-sm">
<i class="fas fa-filter"></i> Filter
</button>
<button type="button" id="clearFilters" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times"></i>
</button>
</div>
<div class="col-12 d-flex justify-content-end mt-3">
<button type="button" id="clearFilters" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-x"></i> Clear Filters
</button>
</div>
</form>
</div>
@ -149,10 +168,10 @@
<div class="d-flex gap-2">
<button type="button" id="refreshLogs" class="btn btn-outline-primary btn-sm">
<i class="fas fa-sync-alt"></i> Refresh
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<button type="button" id="downloadLogs" class="btn btn-outline-success btn-sm">
<i class="fas fa-download"></i> Download CSV
<i class="bi bi-download"></i> Download CSV
</button>
</div>
</div>
@ -167,13 +186,15 @@
<th style="width: 180px;">Action</th>
<th style="width: 100px;">Status</th>
<th>Description</th>
<th style="width: 60px;">Details</th>
</tr>
</thead>
<tbody id="logsTableBody">
<tr>
<td colspan="6" class="text-center py-4">
<i class="fas fa-spinner fa-spin"></i> Loading logs...
<td colspan="5" class="text-center py-4">
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading logs...
</td>
</tr>
</tbody>
@ -226,15 +247,17 @@
document.addEventListener('DOMContentLoaded', function () {
// Initialize the logger manager
window.loggerManager = new LoggerManager({
initialCategory: "{{ category or '' }}",
initialStartDate: "{{ start_date or '' }}",
initialEndDate: "{{ end_date or '' }}",
initialSearchTerm: "{{ search_term or '' }}"
initialFilters: {
category: {{ selected_categories | tojson }},
start_date: "{{ start_date or '' }}",
end_date: "{{ end_date or '' }}",
search_term: "{{ search_term or '' }}"
}
});
// Set up modal handler for log details
const logModal = new ModalHandler('logDetailModal', 'log-detail-content');
window.loggerManager.setModalHandler(logModal);
// Set up modal handler for log details
const logModal = new ModalHandler('logDetailModal', 'log-detail-content');
window.loggerManager.setModalHandler(logModal);
});
</script>
{% endblock scripts %}