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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user