From 4d9d23511140e9e87b8f24d7aff07d415d202ff0 Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Wed, 15 Oct 2025 08:45:31 -0500 Subject: [PATCH] Add sortable columns and inline search filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented sortable table headers with visual indicators (▲▼) - Click any column to sort ascending/descending - Sort state persists during auto-refresh - Added compact inline search filter in toolbar - Unified search across all columns (Sim Source, Artifacts, Date, Uploaded By) - Positioned search on right side of toolbar for cleaner layout - Real-time filtering as you type - Combined filtering and sorting work seamlessly together 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/css/styles.css | 77 ++++++++++++++++++++++++++++++++++++ static/index.html | 25 ++++++++++-- static/js/app.js | 91 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 186 insertions(+), 7 deletions(-) diff --git a/static/css/styles.css b/static/css/styles.css index 145f04a..58bcca3 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -99,6 +99,52 @@ header h1 { align-items: center; } +.filter-inline { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #0f172a; + border-radius: 6px; + border: 1px solid #334155; + min-width: 250px; +} + +.filter-inline input { + flex: 1; + padding: 4px 8px; + background: transparent; + border: none; + color: #e2e8f0; + font-size: 14px; +} + +.filter-inline input:focus { + outline: none; +} + +.filter-inline input::placeholder { + color: #64748b; +} + +.btn-clear { + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 4px; + color: #64748b; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; +} + +.btn-clear:hover { + background: #334155; + color: #e2e8f0; +} + .btn { padding: 10px 20px; border: none; @@ -189,6 +235,33 @@ th { letter-spacing: 0.5px; } +th.sortable { + cursor: pointer; + user-select: none; + transition: color 0.3s; +} + +th.sortable:hover { + color: #60a5fa; +} + +.sort-indicator { + display: inline-block; + margin-left: 5px; + font-size: 10px; + color: #64748b; +} + +th.sort-asc .sort-indicator::after { + content: '▲'; + color: #60a5fa; +} + +th.sort-desc .sort-indicator::after { + content: '▼'; + color: #60a5fa; +} + td { padding: 16px 12px; border-bottom: 1px solid #1e293b; @@ -469,4 +542,8 @@ code { th, td { padding: 8px 6px; } + + .toolbar { + flex-wrap: wrap; + } } diff --git a/static/index.html b/static/index.html index 760855b..eeb02a9 100644 --- a/static/index.html +++ b/static/index.html @@ -41,17 +41,34 @@ + + +
+ + + +
- - - - + + + + diff --git a/static/js/app.js b/static/js/app.js index f752731..29494ab 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -11,6 +11,11 @@ let autoRefreshEnabled = true; let autoRefreshInterval = null; const REFRESH_INTERVAL_MS = 5000; // 5 seconds +// Sorting and filtering +let allArtifacts = []; // Store all artifacts for client-side sorting/filtering +let currentSortColumn = null; +let currentSortDirection = 'asc'; + // Load API info on page load window.addEventListener('DOMContentLoaded', () => { loadApiInfo(); @@ -37,12 +42,13 @@ async function loadArtifacts(limit = pageSize, offset = 0) { const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`); const artifacts = await response.json(); + allArtifacts = artifacts; // Store for sorting/filtering displayArtifacts(artifacts); updatePagination(artifacts.length); } catch (error) { console.error('Error loading artifacts:', error); document.getElementById('artifacts-tbody').innerHTML = ` - `; @@ -59,7 +65,13 @@ function displayArtifacts(artifacts) { return; } - tbody.innerHTML = artifacts.map(artifact => ` + // Apply current sort if active + let displayedArtifacts = artifacts; + if (currentSortColumn) { + displayedArtifacts = applySorting([...artifacts]); + } + + tbody.innerHTML = displayedArtifacts.map(artifact => ` `).join(''); - document.getElementById('artifact-count').textContent = `${artifacts.length} artifacts`; + document.getElementById('artifact-count').textContent = `${displayedArtifacts.length} artifacts`; // Re-initialize Lucide icons for dynamically added content lucide.createIcons(); @@ -504,3 +516,76 @@ function toggleAutoRefresh() { } } } + +// Apply sorting to artifacts array +function applySorting(artifacts) { + if (!currentSortColumn) return artifacts; + + return artifacts.sort((a, b) => { + let aVal = a[currentSortColumn] || ''; + let bVal = b[currentSortColumn] || ''; + + // Handle date sorting + if (currentSortColumn === 'created_at') { + aVal = new Date(aVal).getTime(); + bVal = new Date(bVal).getTime(); + } else { + // String comparison (case insensitive) + aVal = String(aVal).toLowerCase(); + bVal = String(bVal).toLowerCase(); + } + + if (aVal < bVal) return currentSortDirection === 'asc' ? -1 : 1; + if (aVal > bVal) return currentSortDirection === 'asc' ? 1 : -1; + return 0; + }); +} + +// Sorting functionality +function sortTable(column) { + // Toggle sort direction if clicking same column + if (currentSortColumn === column) { + currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; + } else { + currentSortColumn = column; + currentSortDirection = 'asc'; + } + + // Update sort indicators + document.querySelectorAll('th.sortable').forEach(th => { + th.classList.remove('sort-asc', 'sort-desc'); + }); + + const sortedHeader = event.target.closest('th'); + sortedHeader.classList.add(`sort-${currentSortDirection}`); + + // Apply filter and sort + filterTable(); +} + +// Filtering functionality - searches across all columns +function filterTable() { + const searchTerm = document.getElementById('filter-search').value.toLowerCase(); + + const filteredArtifacts = allArtifacts.filter(artifact => { + if (!searchTerm) return true; + + // Search across all relevant fields + const searchableText = [ + artifact.test_suite || '', + artifact.filename || '', + artifact.test_name || '', + formatDate(artifact.created_at) + ].join(' ').toLowerCase(); + + return searchableText.includes(searchTerm); + }); + + displayArtifacts(filteredArtifacts); +} + +// Clear all filters +function clearFilters() { + document.getElementById('filter-search').value = ''; + filterTable(); +}
Sim SourceArtifactsDateUploaded By + Sim Source + + Artifacts + + Date + + Uploaded By + Actions
+
Error loading artifacts: ${error.message}
${artifact.test_suite || '-'} @@ -82,7 +94,7 @@ function displayArtifacts(artifacts) {