From d179e93dfc7e2ca2bcc9a3234b7136f38b0d270c Mon Sep 17 00:00:00 2001 From: paravozz Date: Thu, 2 Oct 2025 02:52:42 +0100 Subject: [PATCH] feature: state filtering + dark theme --- flower/static/css/flower.css | 245 +++++++++++++++++++++++++++ flower/static/js/flower.js | 319 ++++++++++++++++++++++++++++++++++- flower/templates/navbar.html | 14 +- flower/templates/tasks.html | 25 +++ flower/utils/tasks.py | 16 +- flower/views/tasks.py | 42 ++++- 6 files changed, 642 insertions(+), 19 deletions(-) diff --git a/flower/static/css/flower.css b/flower/static/css/flower.css index b7ef3ecde..e00f0c34b 100644 --- a/flower/static/css/flower.css +++ b/flower/static/css/flower.css @@ -39,3 +39,248 @@ div.dataTables_wrapper .dataTables_filter input { .overflow-auto::-webkit-scrollbar { display: none; } + +:root[data-bs-theme='dark'] { + color-scheme: dark; +} + +:root[data-bs-theme='dark'] .bg-green { + background-color: #18301e; +} + +:root[data-bs-theme='dark'] .dropdown-menu, +:root[data-bs-theme='dark'] .form-control, +:root[data-bs-theme='dark'] .btn, +:root[data-bs-theme='dark'] .page-link { + background-color: #121212; + color: #e2e2e2; +} + +:root[data-bs-theme='dark'] body { + background-color: #121212; + color: #e2e2e2; +} + +:root[data-bs-theme='dark'] .table { + color: #e2e2e2; + --bs-table-bg: transparent; + --bs-table-striped-bg: transparent; + --bs-table-striped-color: #e2e2e2; + --bs-table-hover-bg: rgba(255, 255, 255, 0.075); + --bs-table-hover-color: #e2e2e2; +} + +:root[data-bs-theme='dark'] .table thead th { + background-color: #1a1f1a; + color: #e2e2e2; + border-color: #444; +} + +:root[data-bs-theme='dark'] .table-striped > tbody > tr:nth-of-type(odd) > * { + background-color: #151a15 !important; /* subtle deep green tint */ + color: #e2e2e2 !important; +} + +:root[data-bs-theme='dark'] .table-striped > tbody > tr:nth-of-type(even) > * { + background-color: #111411 !important; /* base row color */ + color: #e2e2e2 !important; +} + +:root[data-bs-theme='dark'] .dataTables_wrapper { + background-color: transparent !important; + border-color: #2f3c2a; +} + +:root[data-bs-theme='dark'] .dataTables_wrapper .dataTables_info, +:root[data-bs-theme='dark'] .dataTables_wrapper .dataTables_length label, +:root[data-bs-theme='dark'] .dataTables_wrapper .dataTables_filter label { + color: #cfcfcf; +} + +:root[data-bs-theme='dark'] div.dataTables_wrapper .dataTables_filter input { + background-color: #1f1f1f; + color: #e2e2e2; + border: 1px solid #2f3c2a; +} + +:root[data-bs-theme='dark'] .btn-outline-secondary, +:root[data-bs-theme='dark'] .btn-outline-success, +:root[data-bs-theme='dark'] .btn-outline-danger, +:root[data-bs-theme='dark'] .btn-outline-primary, +:root[data-bs-theme='dark'] .btn-outline-warning, +:root[data-bs-theme='dark'] .btn-outline-dark, +:root[data-bs-theme='dark'] .btn-outline-info { + color: #e2e2e2; + border-color: #3b3b3b; +} + +:root[data-bs-theme='dark'] .btn-outline-secondary:hover, +:root[data-bs-theme='dark'] .btn-outline-success:hover, +:root[data-bs-theme='dark'] .btn-outline-danger:hover, +:root[data-bs-theme='dark'] .btn-outline-primary:hover, +:root[data-bs-theme='dark'] .btn-outline-warning:hover, +:root[data-bs-theme='dark'] .btn-outline-dark:hover, +:root[data-bs-theme='dark'] .btn-outline-info:hover { + background-color: #2a2a2a; +} + +:root[data-bs-theme='dark'] a, +:root[data-bs-theme='dark'] .nav-link, +:root[data-bs-theme='dark'] .page-link { + color: #cde4ff; +} + +:root[data-bs-theme='dark'] .dropdown-menu { + background-color: #1a1a1a; + border-color: #333; +} + +:root[data-bs-theme='dark'] .dropdown-item { + color: #e2e2e2; +} + +:root[data-bs-theme='dark'] .dropdown-item:hover { + background-color: #2a2a2a; +} + +:root[data-bs-theme='dark'] .dataTables_wrapper .dataTables_paginate .paginate_button { + color: #e2e2e2 !important; + border: 1px solid #333 !important; + background: #1a1a1a !important; +} + +:root[data-bs-theme='dark'] .dataTables_wrapper .dataTables_paginate .paginate_button.current, +:root[data-bs-theme='dark'] .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { + color: #fff !important; + background: #2a2a2a !important; +} + +:root[data-bs-theme='dark'] .navbar { + background-color: #18301e !important; +} + +:root[data-bs-theme='dark'] .navbar .navbar-brand, +:root[data-bs-theme='dark'] .navbar .nav-link { + color: #e2e2e2 !important; +} + +:root[data-bs-theme='dark'] .navbar .nav-link:hover { + color: #fff !important; +} + +:root[data-bs-theme='dark'] .nav-tabs { + border-bottom: 1px solid #2f3c2a; +} + +:root[data-bs-theme='dark'] .nav-tabs .nav-link { + color: #e2e2e2; + border: 1px solid transparent; + background-color: transparent; +} + +:root[data-bs-theme='dark'] .nav-tabs .nav-link:hover { + color: #fff; + background-color: #111411; + border-color: #2f3c2a #2f3c2a #2f3c2a; +} + +:root[data-bs-theme='dark'] .nav-tabs .nav-link.active, +:root[data-bs-theme='dark'] .nav-tabs .nav-item.show .nav-link { + color: #fff; + background-color: #18301e; + border-color: #2f3c2a #2f3c2a transparent; +} + +:root[data-bs-theme='dark'] .table tbody tr { + background-color: transparent !important; +} + +:root[data-bs-theme='dark'] .table tbody td { + background-color: transparent !important; + color: #e2e2e2 !important; + border-color: #444 !important; +} + +:root[data-bs-theme='dark'] .table tbody tr:hover td { + background-color: rgba(255, 255, 255, 0.075) !important; +} + +:root[data-bs-theme='dark'] table.dataTable tbody tr { + background-color: transparent !important; +} + +:root[data-bs-theme='dark'] table.dataTable tbody td { + background-color: transparent !important; +} + +:root[data-bs-theme='dark'] .dataTables_empty { + background-color: #151a15 !important; + color: #e2e2e2 !important; +} + +#tasks-table_filter { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +#tasks-table_filter label { + margin-bottom: 0; +} + +.state-filter-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.state-filter-label { + font-size: 0.875rem; + color: #6c757d; +} + +:root[data-bs-theme='dark'] .state-filter-label { + color: #adb5bd; +} + +.status-filter-dropdown .dropdown-menu { + min-width: 210px; + max-width: 260px; + max-height: 420px; + overflow-y: auto; + z-index: 1061; + padding: 0.75rem 1rem; +} + +.status-filter-option { + gap: 0.5rem; + padding: 0.25rem 0; +} + +.status-filter-checkbox { + flex: 0 0 auto; + margin: 0; +} + +.status-filter-option-label { + flex: 1 1 auto; + margin: 0; +} + +div.dataTables_wrapper .dataTables_filter .status-filter-options > .status-filter-option > input[type="checkbox"] { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + margin: 0; +} + +.status-filter-option-label { + flex: 1; +} + +.status-filter-option .status-only { + font-size: 0.75rem; + flex: 0 0 auto; +} diff --git a/flower/static/js/flower.js b/flower/static/js/flower.js index 6edde42bf..ee0dbed2e 100644 --- a/flower/static/js/flower.js +++ b/flower/static/js/flower.js @@ -45,6 +45,38 @@ var flower = (function () { } } + function applyTheme(theme) { + var htmlEl = document.documentElement; + var resolved = theme; + if (!resolved || resolved === 'system') { + var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + resolved = prefersDark ? 'dark' : 'light'; + } + htmlEl.setAttribute('data-bs-theme', resolved); + localStorage.setItem('flower-theme', theme || 'system'); + } + + function initTheme() { + var stored = localStorage.getItem('flower-theme') || 'system'; + applyTheme(stored); + if (window.matchMedia) { + var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + if (mediaQuery) { + var syncSystemTheme = function () { + var current = localStorage.getItem('flower-theme') || 'system'; + if (current === 'system') { + applyTheme('system'); + } + }; + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncSystemTheme); + } else if (typeof mediaQuery.addListener === 'function') { + mediaQuery.addListener(syncSystemTheme); + } + } + } + } + $('#worker-refresh').on('click', function (event) { event.preventDefault(); event.stopPropagation(); @@ -407,6 +439,12 @@ var flower = (function () { // Make bootstrap tabs persistent $(document).ready(function () { + initTheme(); + $(document).on('click', '.theme-choice', function (e) { + e.preventDefault(); + var choice = $(this).data('theme'); + applyTheme(choice); + }); if (location.hash !== '') { $('a[href="' + location.hash + '"]').tab('show'); } @@ -536,7 +574,211 @@ var flower = (function () { return; } - $('#tasks-table').DataTable({ + var stateFilterControl = $('#state-filter-control'); + var statusToggleButton = stateFilterControl.find('#status-filter-toggle'); + var statusOptionsContainer = stateFilterControl.find('#status-filter-options'); + var statusDropdownElement = statusToggleButton.length ? statusToggleButton[0] : null; + var statusDropdownInstance = null; + stateFilterControl = stateFilterControl.detach(); + var statusIdCounter = 0; + var selectionLockedFromUrl = false; + + var defaultStates = [ + 'FAILURE', 'IGNORED', 'PENDING', 'QUEUED', 'RECEIVED', + 'REJECTED', 'RETRY', 'REVOKED', 'SCHEDULED', 'SENT', + 'STARTED', 'SUCCESS', 'UNKNOWN' + ]; + + var knownStates = defaultStates.slice(); + var selectedStates = new Set(knownStates); + + function normalizeState(value) { + if (!value) { + return 'UNKNOWN'; + } + return value.toString().trim().toUpperCase(); + } + + function updateStatusSummary() { + var label; + if (selectedStates.size === 0) { + label = 'None'; + } else if (selectedStates.size === knownStates.length) { + label = 'All'; + } else if (selectedStates.size === 1) { + label = Array.from(selectedStates)[0]; + } else { + label = selectedStates.size + ' selected'; + } + statusToggleButton.text(label); + statusToggleButton.attr('aria-label', 'Filter task states. Currently: ' + label + '.'); + } + + function buildOption(state) { + var safeState = normalizeState(state); + var optionId = 'status-filter-' + (safeState.replace(/[^a-z0-9_-]/gi, '').toLowerCase() || ('state-' + statusIdCounter)); + statusIdCounter += 1; + + var option = $('
', { + 'class': 'status-filter-option d-flex align-items-center', + 'data-state': safeState + }); + + var checkbox = $('', { + type: 'checkbox', + 'class': 'form-check-input status-filter-checkbox me-2', + id: optionId, + value: safeState + }).prop('checked', selectedStates.has(safeState)); + + var label = $('
+