next_updates
This commit is contained in:
@@ -9,32 +9,33 @@
|
||||
<h3>Test Jobs</h3>
|
||||
<a href="{{ url_for('jobs.submit') }}" class="btn btn-primary btn-sm">+ New Job</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="job-list">
|
||||
{% if jobs %}
|
||||
{% for job in jobs %}
|
||||
<div class="job-item" data-job-id="{{ job.id }}" onclick="loadJobDetails({{ job.id }})" oncontextmenu="showContextMenu(event, {{ job.id }}, '{{ job.status }}')">
|
||||
<div class="job-status-icon">{{ job.get_status_icon() }}</div>
|
||||
<div class="job-info">
|
||||
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
|
||||
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
|
||||
</div>
|
||||
{% for job in jobs %}
|
||||
<div class="job-item" data-job-id="{{ job.id }}" onclick="loadJobDetails({{ job.id }})"
|
||||
oncontextmenu="showContextMenu(event, {{ job.id }}, '{{ job.status }}')">
|
||||
<div class="job-status-icon">{{ job.get_status_icon() }}</div>
|
||||
<div class="job-info">
|
||||
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
|
||||
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h3>No jobs yet</h3>
|
||||
<p>Submit your first test job to get started</p>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<h3>No jobs yet</h3>
|
||||
<p>Submit your first test job to get started</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Job Details</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="job-details-container">
|
||||
<div class="empty-state">
|
||||
<h3>Select a job</h3>
|
||||
@@ -53,61 +54,90 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let contextJobId = null;
|
||||
let contextJobId = null;
|
||||
|
||||
function showContextMenu(event, jobId, status) {
|
||||
event.preventDefault();
|
||||
|
||||
// Only show context menu for in_progress jobs
|
||||
if (status !== 'in_progress') {
|
||||
return;
|
||||
function showContextMenu(event, jobId, status) {
|
||||
event.preventDefault();
|
||||
|
||||
// Only show context menu for in_progress jobs
|
||||
if (status !== 'in_progress') {
|
||||
return;
|
||||
}
|
||||
|
||||
contextJobId = jobId;
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
|
||||
contextMenu.style.display = 'block';
|
||||
contextMenu.style.left = event.pageX + 'px';
|
||||
contextMenu.style.top = event.pageY + 'px';
|
||||
|
||||
// Hide context menu when clicking elsewhere
|
||||
document.addEventListener('click', hideContextMenu);
|
||||
}
|
||||
|
||||
contextJobId = jobId;
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
|
||||
contextMenu.style.display = 'block';
|
||||
contextMenu.style.left = event.pageX + 'px';
|
||||
contextMenu.style.top = event.pageY + 'px';
|
||||
|
||||
// Hide context menu when clicking elsewhere
|
||||
document.addEventListener('click', hideContextMenu);
|
||||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
document.getElementById('contextMenu').style.display = 'none';
|
||||
document.removeEventListener('click', hideContextMenu);
|
||||
contextJobId = null;
|
||||
}
|
||||
|
||||
function abortJobFromContext() {
|
||||
if (contextJobId) {
|
||||
abortJob(contextJobId);
|
||||
function hideContextMenu() {
|
||||
document.getElementById('contextMenu').style.display = 'none';
|
||||
document.removeEventListener('click', hideContextMenu);
|
||||
contextJobId = null;
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
function loadJobDetails(jobId) {
|
||||
// Mark job as active
|
||||
document.querySelectorAll('.job-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-job-id="${jobId}"]`).classList.add('active');
|
||||
|
||||
// Fetch job details
|
||||
fetch(`/jobs/${jobId}`)
|
||||
.then(response => response.json())
|
||||
.then(job => {
|
||||
const container = document.getElementById('job-details-container');
|
||||
const scenarios = JSON.parse(job.scenarios || '[]');
|
||||
|
||||
container.innerHTML = `
|
||||
|
||||
function abortJobFromContext() {
|
||||
if (contextJobId) {
|
||||
abortJob(contextJobId);
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
let pollingInterval = null;
|
||||
|
||||
function loadJobDetails(jobId) {
|
||||
// Clear existing polling
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
|
||||
// Mark job as active
|
||||
document.querySelectorAll('.job-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
const jobItem = document.querySelector(`[data-job-id="${jobId}"]`);
|
||||
if (jobItem) jobItem.classList.add('active');
|
||||
|
||||
// Initial fetch
|
||||
fetchJobDetails(jobId);
|
||||
|
||||
// Start polling if job is not finished
|
||||
pollingInterval = setInterval(() => {
|
||||
fetchJobDetails(jobId, true);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function fetchJobDetails(jobId, isPolling = false) {
|
||||
fetch(`/jobs/${jobId}`)
|
||||
.then(response => response.json())
|
||||
.then(job => {
|
||||
const container = document.getElementById('job-details-container');
|
||||
const scenarios = JSON.parse(job.scenarios || '[]');
|
||||
const taskIds = JSON.parse(job.remote_task_ids || '{}');
|
||||
const results = JSON.parse(job.remote_results || '{}');
|
||||
|
||||
// If polling and job is finished, stop polling
|
||||
if (isPolling && ['passed', 'failed', 'aborted'].includes(job.status)) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
|
||||
// Update job item icon in the list if it changed
|
||||
const jobItem = document.querySelector(`[data-job-id="${jobId}"]`);
|
||||
if (jobItem) {
|
||||
// We need to fetch the icon separately or update it here
|
||||
// For now, let's just update the details panel
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Job ID:</div>
|
||||
<div class="detail-value">#${job.id}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Submitter:</div>
|
||||
<div class="detail-value">${job.submitter}</div>
|
||||
<div class="detail-value">#${job.id} (Remote: ${job.remote_queue_id || 'N/A'})</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Branch:</div>
|
||||
@@ -115,62 +145,114 @@ function loadJobDetails(jobId) {
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Status:</div>
|
||||
<div class="detail-value">
|
||||
<div class="detail-value" id="job-status-badge">
|
||||
<span class="status-badge status-${job.status}">${job.status.replace('_', ' ').toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Environment:</div>
|
||||
<div class="detail-value">${job.environment}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Test Mode:</div>
|
||||
<div class="detail-value">${job.test_mode}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Submitted:</div>
|
||||
<div class="detail-value">${job.submitted_at}</div>
|
||||
</div>
|
||||
${job.completed_at ? `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Completed:</div>
|
||||
<div class="detail-value">${job.completed_at}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Duration:</div>
|
||||
<div class="detail-value">${job.duration ? Math.floor(job.duration / 60) + 'm ' + (job.duration % 60) + 's' : 'N/A'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Scenarios:</div>
|
||||
<div class="detail-value">${Array.isArray(scenarios) ? scenarios.join(', ') : scenarios}</div>
|
||||
</div>
|
||||
${job.status === 'in_progress' ? `
|
||||
<div style="margin-top: 20px;">
|
||||
<button class="btn btn-danger" onclick="abortJob(${job.id})">Abort Job</button>
|
||||
</div>
|
||||
` : ''}
|
||||
${job.results_path ? `
|
||||
<div style="margin-top: 20px;">
|
||||
<a href="${job.results_path}" class="btn btn-primary" target="_blank">View Results</a>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||
<h4 style="color: var(--dark);">Scenario Summary</h4>
|
||||
<input type="text" id="scenarioSearch" placeholder="Search scenarios..." onkeyup="filterScenarios()" class="form-control" style="width: 200px; padding: 6px 12px;">
|
||||
</div>
|
||||
<table class="user-table" id="scenarioTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Scenario Name</th>
|
||||
<th>Task ID</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function abortJob(jobId) {
|
||||
if (confirm('Are you sure you want to abort this job?')) {
|
||||
fetch(`/jobs/${jobId}/abort`, { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error);
|
||||
scenarios.forEach(s => {
|
||||
const taskId = taskIds[s] || 'N/A';
|
||||
const result = results[s]; // [status, link]
|
||||
let statusHtml = '⌛ Waiting';
|
||||
|
||||
if (result) {
|
||||
const icon = result[0] === 'PASS' ? '✅' : '❌';
|
||||
const color = result[0] === 'PASS' ? 'var(--success)' : 'var(--danger)';
|
||||
statusHtml = `<a href="${result[1]}" target="_blank" style="text-decoration: none; color: ${color}; font-weight: 600;">${icon} ${result[0]}</a>`;
|
||||
} else if (job.status === 'in_progress') {
|
||||
statusHtml = '🔄 Running';
|
||||
}
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${s}</td>
|
||||
<td><small>${taskId}</small></td>
|
||||
<td>${statusHtml}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<h4 style="color: var(--dark); margin-bottom: 15px;">Queue Logging</h4>
|
||||
<div id="queue-log" style="background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; max-height: 400px; overflow-y: auto; white-space: pre-wrap;">${job.queue_log || 'Waiting for logs...'}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; display: flex; gap: 10px;">
|
||||
<button class="btn btn-danger" onclick="abortJob(${job.id})" style="width: auto;">Abort Job</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Scroll log to bottom if it's being updated
|
||||
const logElement = document.getElementById('queue-log');
|
||||
if (logElement) logElement.scrollTop = logElement.scrollHeight;
|
||||
|
||||
// If in progress, trigger status update on backend
|
||||
if (!['passed', 'failed', 'aborted'].includes(job.status)) {
|
||||
fetch(`/jobs/${jobId}/status`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
// Update icon in list
|
||||
const iconEl = document.querySelector(`[data-job-id="${jobId}"] .job-status-icon`);
|
||||
if (iconEl) iconEl.textContent = data.status_icon;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function filterScenarios() {
|
||||
const input = document.getElementById('scenarioSearch');
|
||||
const filter = input.value.toUpperCase();
|
||||
const table = document.getElementById('scenarioTable');
|
||||
const tr = table.getElementsByTagName('tr');
|
||||
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
const td = tr[i].getElementsByTagName('td')[0];
|
||||
if (td) {
|
||||
const txtValue = td.textContent || td.innerText;
|
||||
if (txtValue.toUpperCase().indexOf(filter) > -1) {
|
||||
tr[i].style.display = "";
|
||||
} else {
|
||||
tr[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function abortJob(jobId) {
|
||||
if (confirm('Are you sure you want to abort this job?')) {
|
||||
fetch(`/jobs/${jobId}/abort`, { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user