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 @@
+
+
+
+
+
+
+
- | Sim Source |
- Artifacts |
- Date |
- Uploaded By |
+
+ Sim Source
+ |
+
+ Artifacts
+ |
+
+ Date
+ |
+
+ Uploaded By
+ |
Actions |
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 = `
- |
+ |
|
Error loading artifacts: ${error.message}
|
`;
@@ -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 => `
| ${artifact.test_suite || '-'} |
@@ -82,7 +94,7 @@ function displayArtifacts(artifacts) {
|
`).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();
+}