Phase 8: Execution Search & Filtering
Added comprehensive search and filtering for execution history: - Search bar to filter by command text, execution ID, or workflow name - Status filter dropdown (All, Running, Completed, Failed, Waiting) - Real-time client-side filtering as user types - Filter statistics showing X of Y executions - Clear Filters button to reset all filters - Extracts command text from logs for quick command searches - Maintains all executions in memory for instant filtering - Terminal-themed filter UI matching existing aesthetic Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -809,6 +809,37 @@
|
|||||||
<div id="executions" class="tab-content">
|
<div id="executions" class="tab-content">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Execution History</h3>
|
<h3>Execution History</h3>
|
||||||
|
|
||||||
|
<!-- Search and Filter Section -->
|
||||||
|
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px; margin-bottom: 20px;">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||||||
|
<!-- Search Box -->
|
||||||
|
<div>
|
||||||
|
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--terminal-amber);">🔍 Search:</label>
|
||||||
|
<input type="text" id="executionSearch" placeholder="Search by command, execution ID, or workflow name..."
|
||||||
|
oninput="filterExecutions()"
|
||||||
|
style="width: 100%; padding: 10px; margin: 0;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Filter -->
|
||||||
|
<div>
|
||||||
|
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--terminal-amber);">📊 Status Filter:</label>
|
||||||
|
<select id="statusFilter" onchange="filterExecutions()" style="width: 100%; padding: 10px; margin: 0;">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="running">Running</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="failed">Failed</option>
|
||||||
|
<option value="waiting">Waiting</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
|
<button onclick="clearFilters()" class="small">[ Clear Filters ]</button>
|
||||||
|
<span id="filterStats" style="color: var(--terminal-green); font-size: 0.9em; font-family: var(--font-mono);"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onclick="refreshData()">[ 🔄 Refresh ]</button>
|
<button onclick="refreshData()">[ 🔄 Refresh ]</button>
|
||||||
<button onclick="clearCompletedExecutions()" style="margin-left: 10px;">[ 🗑️ Clear Completed ]</button>
|
<button onclick="clearCompletedExecutions()" style="margin-left: 10px;">[ 🗑️ Clear Completed ]</button>
|
||||||
<div id="executionList"><div class="loading">Loading...</div></div>
|
<div id="executionList"><div class="loading">Loading...</div></div>
|
||||||
@@ -920,6 +951,7 @@
|
|||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let workers = [];
|
let workers = [];
|
||||||
|
let allExecutions = []; // Store all loaded executions for filtering
|
||||||
|
|
||||||
async function loadUser() {
|
async function loadUser() {
|
||||||
try {
|
try {
|
||||||
@@ -1080,6 +1112,13 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const executions = data.executions || data; // Handle old and new API format
|
const executions = data.executions || data; // Handle old and new API format
|
||||||
|
|
||||||
|
// Store executions for filtering
|
||||||
|
if (append) {
|
||||||
|
allExecutions = allExecutions.concat(executions);
|
||||||
|
} else {
|
||||||
|
allExecutions = executions;
|
||||||
|
}
|
||||||
|
|
||||||
// Dashboard view (always first 5)
|
// Dashboard view (always first 5)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
const dashHtml = executions.length === 0 ?
|
const dashHtml = executions.length === 0 ?
|
||||||
@@ -1094,25 +1133,8 @@
|
|||||||
document.getElementById('dashExecutions').innerHTML = dashHtml;
|
document.getElementById('dashExecutions').innerHTML = dashHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full execution list
|
// Apply filters and render
|
||||||
const fullHtml = executions.length === 0 ?
|
renderFilteredExecutions();
|
||||||
'<div class="empty">No executions yet</div>' :
|
|
||||||
executions.map(e => `
|
|
||||||
<div class="execution-item" onclick="viewExecution('${e.id}')">
|
|
||||||
<span class="status ${e.status}">${e.status}</span>
|
|
||||||
<strong>${e.workflow_name || '[Quick Command]'}</strong>
|
|
||||||
<div class="timestamp">
|
|
||||||
Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
|
|
||||||
${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
if (append) {
|
|
||||||
document.getElementById('executionList').innerHTML += fullHtml;
|
|
||||||
} else {
|
|
||||||
document.getElementById('executionList').innerHTML = fullHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add "Load More" button if there are more executions
|
// Add "Load More" button if there are more executions
|
||||||
if (data.hasMore) {
|
if (data.hasMore) {
|
||||||
@@ -1125,6 +1147,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFilteredExecutions() {
|
||||||
|
const searchTerm = (document.getElementById('executionSearch')?.value || '').toLowerCase();
|
||||||
|
const statusFilter = document.getElementById('statusFilter')?.value || '';
|
||||||
|
|
||||||
|
// Filter executions
|
||||||
|
let filtered = allExecutions.filter(e => {
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter && e.status !== statusFilter) return false;
|
||||||
|
|
||||||
|
// Search filter (search in workflow name, execution ID, and logs)
|
||||||
|
if (searchTerm) {
|
||||||
|
const workflowName = (e.workflow_name || '[Quick Command]').toLowerCase();
|
||||||
|
const executionId = e.id.toLowerCase();
|
||||||
|
|
||||||
|
// Try to extract command from logs if it's a quick command
|
||||||
|
let commandText = '';
|
||||||
|
try {
|
||||||
|
const logs = typeof e.logs === 'string' ? JSON.parse(e.logs) : e.logs;
|
||||||
|
if (logs && logs.length > 0 && logs[0].command) {
|
||||||
|
commandText = logs[0].command.toLowerCase();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore parsing errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchFound = workflowName.includes(searchTerm) ||
|
||||||
|
executionId.includes(searchTerm) ||
|
||||||
|
commandText.includes(searchTerm);
|
||||||
|
|
||||||
|
if (!matchFound) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update filter stats
|
||||||
|
const statsEl = document.getElementById('filterStats');
|
||||||
|
if (statsEl) {
|
||||||
|
if (searchTerm || statusFilter) {
|
||||||
|
statsEl.textContent = `Showing ${filtered.length} of ${allExecutions.length} executions`;
|
||||||
|
} else {
|
||||||
|
statsEl.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render filtered results
|
||||||
|
const fullHtml = filtered.length === 0 ?
|
||||||
|
'<div class="empty">No executions match your filters</div>' :
|
||||||
|
filtered.map(e => `
|
||||||
|
<div class="execution-item" onclick="viewExecution('${e.id}')">
|
||||||
|
<span class="status ${e.status}">${e.status}</span>
|
||||||
|
<strong>${e.workflow_name || '[Quick Command]'}</strong>
|
||||||
|
<div class="timestamp">
|
||||||
|
Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
|
||||||
|
${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
document.getElementById('executionList').innerHTML = fullHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterExecutions() {
|
||||||
|
renderFilteredExecutions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('executionSearch').value = '';
|
||||||
|
document.getElementById('statusFilter').value = '';
|
||||||
|
renderFilteredExecutions();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMoreExecutions() {
|
async function loadMoreExecutions() {
|
||||||
executionOffset += executionLimit;
|
executionOffset += executionLimit;
|
||||||
await loadExecutions(true);
|
await loadExecutions(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user