Add sortable columns and inline search filter

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-10-15 08:45:31 -05:00
parent 4bc8310070
commit 4d9d235111
3 changed files with 186 additions and 7 deletions

View File

@@ -99,6 +99,52 @@ header h1 {
align-items: center; 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 { .btn {
padding: 10px 20px; padding: 10px 20px;
border: none; border: none;
@@ -189,6 +235,33 @@ th {
letter-spacing: 0.5px; 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 { td {
padding: 16px 12px; padding: 16px 12px;
border-bottom: 1px solid #1e293b; border-bottom: 1px solid #1e293b;
@@ -469,4 +542,8 @@ code {
th, td { th, td {
padding: 8px 6px; padding: 8px 6px;
} }
.toolbar {
flex-wrap: wrap;
}
} }

View File

@@ -41,17 +41,34 @@
<button onclick="generateSeedData()" class="btn btn-secondary"> <button onclick="generateSeedData()" class="btn btn-secondary">
<i data-lucide="sparkles" style="width: 16px; height: 16px;"></i> Generate Seed Data <i data-lucide="sparkles" style="width: 16px; height: 16px;"></i> Generate Seed Data
</button> </button>
<span id="artifact-count" class="count-badge"></span> <span id="artifact-count" class="count-badge"></span>
<div class="filter-inline">
<i data-lucide="search" style="width: 16px; height: 16px; color: #64748b;"></i>
<input type="text" id="filter-search" placeholder="Search..." oninput="filterTable()">
<button onclick="clearFilters()" class="btn-clear" title="Clear search">
<i data-lucide="x" style="width: 14px; height: 14px;"></i>
</button>
</div>
</div> </div>
<div class="table-container"> <div class="table-container">
<table id="artifacts-table"> <table id="artifacts-table">
<thead> <thead>
<tr> <tr>
<th>Sim Source</th> <th class="sortable" onclick="sortTable('test_suite')">
<th>Artifacts</th> Sim Source <span class="sort-indicator"></span>
<th>Date</th> </th>
<th>Uploaded By</th> <th class="sortable" onclick="sortTable('filename')">
Artifacts <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('created_at')">
Date <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('test_name')">
Uploaded By <span class="sort-indicator"></span>
</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>

View File

@@ -11,6 +11,11 @@ let autoRefreshEnabled = true;
let autoRefreshInterval = null; let autoRefreshInterval = null;
const REFRESH_INTERVAL_MS = 5000; // 5 seconds 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 // Load API info on page load
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
loadApiInfo(); loadApiInfo();
@@ -37,12 +42,13 @@ async function loadArtifacts(limit = pageSize, offset = 0) {
const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`); const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`);
const artifacts = await response.json(); const artifacts = await response.json();
allArtifacts = artifacts; // Store for sorting/filtering
displayArtifacts(artifacts); displayArtifacts(artifacts);
updatePagination(artifacts.length); updatePagination(artifacts.length);
} catch (error) { } catch (error) {
console.error('Error loading artifacts:', error); console.error('Error loading artifacts:', error);
document.getElementById('artifacts-tbody').innerHTML = ` document.getElementById('artifacts-tbody').innerHTML = `
<tr><td colspan="10" class="loading" style="color: #ef4444;"> <tr><td colspan="5" class="loading" style="color: #ef4444;">
Error loading artifacts: ${error.message} Error loading artifacts: ${error.message}
</td></tr> </td></tr>
`; `;
@@ -59,7 +65,13 @@ function displayArtifacts(artifacts) {
return; return;
} }
tbody.innerHTML = artifacts.map(artifact => ` // Apply current sort if active
let displayedArtifacts = artifacts;
if (currentSortColumn) {
displayedArtifacts = applySorting([...artifacts]);
}
tbody.innerHTML = displayedArtifacts.map(artifact => `
<tr> <tr>
<td>${artifact.test_suite || '-'}</td> <td>${artifact.test_suite || '-'}</td>
<td> <td>
@@ -82,7 +94,7 @@ function displayArtifacts(artifacts) {
</tr> </tr>
`).join(''); `).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 // Re-initialize Lucide icons for dynamically added content
lucide.createIcons(); 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();
}