From b4a7855acb05529ec9f829926ba520e4346c9d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Sampaio?= Date: Thu, 24 Sep 2026 17:06:29 -0600 Subject: [PATCH 1/3] Reduce number of DOM reads --- .../templates/tasktiger_admin/tasktiger.html | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html index 6327e9f..12f20a5 100644 --- a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html +++ b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html @@ -120,42 +120,48 @@

TaskTiger

function applySort() { const rows = Array.from(searchableBody.rows); + // Read the sort keys from the DOM once per row, so that the comparator + // works only on plain values. With no active sort, all values are equal + // and the original index alone decides the order. + const toEntry = row => ({ + row, + value: sortColumn === null ? 0 : cellValue(row, sortColumn), + originalIndex: parseInt(row.dataset.originalIndex, 10), + }); + // 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: [] }; + currentBlock = { header: toEntry(row), children: [] }; blocks.push(currentBlock); } else if (currentBlock) { - currentBlock.children.push(row); + currentBlock.children.push(toEntry(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, sortColumn) - cellValue(b, sortColumn); + const delta = a.value - b.value; const ordered = sortDirection === 'asc' ? delta : -delta; - return ordered !== 0 ? ordered : originalIndex(a) - originalIndex(b); + return ordered !== 0 ? ordered : a.originalIndex - b.originalIndex; }; 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. + // Collect the rows in the new order in a fragment, then insert the + // fragment into the table body in a single operation. The rows move + // with their inline display, so collapse/filter visibility is kept. + const fragment = document.createDocumentFragment(); blocks.forEach(block => { - searchableBody.appendChild(block.header); - block.children.forEach(child => searchableBody.appendChild(child)); + fragment.appendChild(block.header.row); + block.children.forEach(child => fragment.appendChild(child.row)); }); + searchableBody.appendChild(fragment); } function updateArrows() { From 91fbc3b7b9eda96dde5a8992cb6dbecec6f48f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Sampaio?= Date: Thu, 24 Sep 2026 17:19:10 -0600 Subject: [PATCH 2/3] Only read from the DOM on first sorting action --- .../templates/tasktiger_admin/tasktiger.html | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html index 12f20a5..1620aff 100644 --- a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html +++ b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html @@ -76,11 +76,7 @@

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; @@ -112,52 +108,68 @@

TaskTiger

return row.cells[0].tagName === 'TH'; } -// Parse the numeric value of a row's cell at the given column index. -function cellValue(row, column) { - return parseInt(row.cells[column].textContent.trim(), 10) || 0; +// Parse the numeric value of a cell. Text that is not a number gives 0. +function cellValue(cell) { + return parseInt(cell.textContent.trim(), 10) || 0; } -function applySort() { - const rows = Array.from(searchableBody.rows); - - // Read the sort keys from the DOM once per row, so that the comparator - // works only on plain values. With no active sort, all values are equal - // and the original index alone decides the order. - const toEntry = row => ({ - row, - value: sortColumn === null ? 0 : cellValue(row, sortColumn), - originalIndex: parseInt(row.dataset.originalIndex, 10), - }); +// Cache for `getBlocks()`. +let cachedBlocks = null; + +// Return the rows of the table body, grouped in blocks. A block begins at a +// top-level row and includes all following child rows up to the next +// top-level row. The first call reads every row from the DOM. Later calls +// return the same blocks and do not read the DOM. The counts do not change +// until the page reloads, so the cached values stay correct. The first call +// must happen before the rows move, so that the blocks keep the original +// row order. +function getBlocks() { + if (cachedBlocks !== null) { + return cachedBlocks; + } - // 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 = []; + cachedBlocks = []; let currentBlock = null; - rows.forEach(row => { + Array.from(searchableBody.rows).forEach(row => { + // The values have the same indexes as `row.cells`, so that the + // `cellIndex` of a sort header selects the value of its column. + const entry = { row, values: Array.from(row.cells, cellValue) }; if (isTopLevelRow(row)) { - currentBlock = { header: toEntry(row), children: [] }; - blocks.push(currentBlock); + currentBlock = { header: entry, children: [] }; + cachedBlocks.push(currentBlock); } else if (currentBlock) { - currentBlock.children.push(toEntry(row)); + currentBlock.children.push(entry); } }); + return cachedBlocks; +} - // 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) => { - const delta = a.value - b.value; - const ordered = sortDirection === 'asc' ? delta : -delta; - return ordered !== 0 ? ordered : a.originalIndex - b.originalIndex; - }; - - blocks.sort((a, b) => compare(a.header, b.header)); - blocks.forEach(block => block.children.sort(compare)); +function applySort() { + const blocks = getBlocks(); + + // With no active sort, use the blocks in their original order. + // Otherwise, sort copies of the blocks by the value of the sort column. + // `Array.prototype.sort` is stable and the copies start in the original + // order, so equal values keep their original order. + let orderedBlocks = blocks; + if (sortDirection !== null) { + const compare = (a, b) => { + const delta = a.values[sortColumn] - b.values[sortColumn]; + return sortDirection === 'asc' ? delta : -delta; + }; + orderedBlocks = blocks + .map(block => ({ + header: block.header, + children: block.children.slice().sort(compare), + })) + .sort((a, b) => compare(a.header, b.header)); + } // Collect the rows in the new order in a fragment, then insert the // fragment into the table body in a single operation. The rows move // with their inline display, so collapse/filter visibility is kept. const fragment = document.createDocumentFragment(); - blocks.forEach(block => { + orderedBlocks.forEach(block => { fragment.appendChild(block.header.row); block.children.forEach(child => fragment.appendChild(child.row)); }); From e20589ddd7a0176d0ab927930157bb596b90d8a3 Mon Sep 17 00:00:00 2001 From: Thomas Steinacher Date: Fri, 25 Sep 2026 12:59:01 -0500 Subject: [PATCH 3/3] Sort queue groups without moving collapsed rows --- .../templates/tasktiger_admin/tasktiger.html | 139 ++++++++++-------- 1 file changed, 76 insertions(+), 63 deletions(-) diff --git a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html index 2214286..6d7e7e0 100644 --- a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html +++ b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html @@ -15,6 +15,10 @@

TaskTiger

.sort-header { cursor: pointer; } + .metrics.table-striped > tbody.searchable:nth-of-type(even) > tr:first-child > th, + .metrics.table-striped > tbody.searchable:nth-of-type(even) > tr:first-child > td { + background-color: transparent; + } @@ -27,8 +31,8 @@

TaskTiger

- {% for group_name, group_stats, queue_stats in queue_stats_groups %} + {% if queue_stats|length > 1 %} @@ -52,8 +56,8 @@

TaskTiger

{% endfor %} -{% endfor %} +{% endfor %}
Error
► {{ group_name }} ({{ queue_stats|length }}) {{ stats.error }}
{% endblock %} @@ -61,7 +65,6 @@

TaskTiger

{% block tail %}