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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +41,34 @@
|
||||
<button onclick="generateSeedData()" class="btn btn-secondary">
|
||||
<i data-lucide="sparkles" style="width: 16px; height: 16px;"></i> Generate Seed Data
|
||||
</button>
|
||||
|
||||
<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 class="table-container">
|
||||
<table id="artifacts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sim Source</th>
|
||||
<th>Artifacts</th>
|
||||
<th>Date</th>
|
||||
<th>Uploaded By</th>
|
||||
<th class="sortable" onclick="sortTable('test_suite')">
|
||||
Sim Source <span class="sort-indicator"></span>
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -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 = `
|
||||
<tr><td colspan="10" class="loading" style="color: #ef4444;">
|
||||
<tr><td colspan="5" class="loading" style="color: #ef4444;">
|
||||
Error loading artifacts: ${error.message}
|
||||
</td></tr>
|
||||
`;
|
||||
@@ -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 => `
|
||||
<tr>
|
||||
<td>${artifact.test_suite || '-'}</td>
|
||||
<td>
|
||||
@@ -82,7 +94,7 @@ function displayArtifacts(artifacts) {
|
||||
</tr>
|
||||
`).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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user