From f020b3340d8c11f577bb922e2391a4163643db35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Sampaio?= Date: Mon, 29 Jun 2026 13:01:54 -0600 Subject: [PATCH] Implement sorting for TaskTiger queue stats --- CHANGELOG.md | 7 ++ tasktiger_admin/__init__.py | 2 +- .../templates/tasktiger_admin/tasktiger.html | 104 +++++++++++++++++- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb93bf..bd2f6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changes +## v0.4.2 + +* Make the task-state columns (Queued, Active, Scheduled, Error) sortable. + Clicking a column header cycles its sort through descending, ascending, and + the original order, with a `↓`/`↑` arrow showing the active direction. + Grouped rows keep their children attached and sorted under their parent. + ## v0.4.1 * First release with automated releases. \ No newline at end of file diff --git a/tasktiger_admin/__init__.py b/tasktiger_admin/__init__.py index 0ea0b01..1adf5e7 100644 --- a/tasktiger_admin/__init__.py +++ b/tasktiger_admin/__init__.py @@ -2,7 +2,7 @@ from .views import TaskTigerView -__version__ = "0.4.1" +__version__ = "0.4.2" __all__ = ["TaskTigerView", "tasktiger_admin"] tasktiger_admin = Blueprint( diff --git a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html index 7ca1c6b..f37b56e 100644 --- a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html +++ b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html @@ -15,10 +15,10 @@

TaskTiger

- Queued - Active - Scheduled - Error + Queued + Active + Scheduled + Error @@ -70,6 +70,102 @@

TaskTiger

}); }); +// Capture the original row order once so the "none" sort mode can restore it. +const searchableBody = document.querySelector('tbody.searchable'); +Array.from(searchableBody.rows).forEach((row, index) => { + row.dataset.originalIndex = index; +}); + +// Sorting state for the task-state columns. +let sortColumn = null; +let sortDirection = null; + +document.querySelectorAll('.sort-header').forEach(header => { + header.addEventListener('click', function() { + const column = parseInt(header.dataset.col, 10); + if (sortColumn === column) { + // Cycle the active column: desc -> asc -> none. + if (sortDirection === 'desc') { + sortDirection = 'asc'; + } else if (sortDirection === 'asc') { + sortColumn = null; + sortDirection = null; + } + } else { + // Move the sort to a new column, starting descending. + sortColumn = column; + sortDirection = 'desc'; + } + applySort(); + updateArrows(); + }); +}); + +// Return true if the row is a top-level row (its first cell is a ). +function isTopLevelRow(row) { + return row.cells[0].tagName === 'TH'; +} + +// Parse the numeric value of a row's cell at the active sort column. +function cellValue(row) { + return parseInt(row.cells[sortColumn].textContent.trim(), 10) || 0; +} + +function applySort() { + const rows = Array.from(searchableBody.rows); + + // Partition rows into blocks. A block begins at a top-level row and + // includes all following child rows up to the next top-level row. + const blocks = []; + let currentBlock = null; + rows.forEach(row => { + if (isTopLevelRow(row)) { + currentBlock = { header: row, children: [] }; + blocks.push(currentBlock); + } else if (currentBlock) { + currentBlock.children.push(row); + } + }); + + const originalIndex = row => + parseInt(row.dataset.originalIndex, 10); + + // Comparator for the current direction, with a stable tie-break on the + // original index so equal values keep a deterministic order. + const compare = (a, b) => { + if (sortDirection === null) { + return originalIndex(a) - originalIndex(b); + } + const delta = cellValue(a) - cellValue(b); + const ordered = sortDirection === 'asc' ? delta : -delta; + return ordered !== 0 ? ordered : originalIndex(a) - originalIndex(b); + }; + + blocks.sort((a, b) => compare(a.header, b.header)); + blocks.forEach(block => block.children.sort(compare)); + + // Re-append rows in the new order. appendChild moves nodes, preserving + // each row's inline display so collapse/filter visibility is kept. + blocks.forEach(block => { + searchableBody.appendChild(block.header); + block.children.forEach(child => searchableBody.appendChild(child)); + }); +} + +function updateArrows() { + document.querySelectorAll('.sort-header').forEach(header => { + const arrow = header.querySelector('.sort-arrow'); + const column = parseInt(header.dataset.col, 10); + if (column === sortColumn && sortDirection === 'desc') { + arrow.textContent = '↓'; + } else if (column === sortColumn && sortDirection === 'asc') { + arrow.textContent = '↑'; + } else { + arrow.textContent = ''; + } + }); +} + function filterRows(value) { const regex = new RegExp(value, 'i'); const rows = document.querySelectorAll('.searchable tr');