next_updates

This commit is contained in:
2025-12-28 05:52:49 +01:00
parent 986f3ae5b5
commit 6b254534f6
8 changed files with 766 additions and 533 deletions

View File

@@ -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 %}

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="submit-container">
<h2 style="margin-bottom: 30px; color: var(--dark);">Submit New Test Job</h2>
<div class="step-indicator">
<div class="step active">
<div class="step-number">1</div>
@@ -17,181 +17,174 @@
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-label">Environment</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-label">Test Mode</div>
</div>
<div class="step">
<div class="step-number">5</div>
<div class="step-label">Review</div>
</div>
</div>
<form id="branchForm">
<div class="form-group">
<label for="branch_name">Git Branch Name</label>
<input type="text" id="branch_name" name="branch_name" class="form-control"
placeholder="e.g., feature/new-feature" required>
<input type="text" id="branch_name" name="branch_name" class="form-control"
placeholder="e.g., feature/new-feature" required>
<small style="color: #6b7280; margin-top: 5px; display: block;">
Enter the branch name to analyze available test scenarios
</small>
<div id="branchValidation" class="branch-validation">
<div class="loading-spinner"></div>
<span id="validationMessage">Validating branch...</span>
</div>
</div>
<div class="form-actions">
<a href="{{ url_for('dashboard.index') }}" class="btn" style="background: #6b7280; color: white;">Cancel</a>
<button type="submit" id="validateBtn" class="btn btn-primary">Validate Branch</button>
<button type="button" id="nextBtn" class="btn btn-primary" style="display: none;" onclick="proceedToStep2()">Next</button>
<button type="button" id="nextBtn" class="btn btn-primary" style="display: none;"
onclick="proceedToStep2()">Next</button>
</div>
</form>
</div>
<script>
let validatedBranch = null;
let organizedData = {};
let scenarioMap = {};
let validatedBranch = null;
let organizedData = {};
let scenarioMap = {};
document.getElementById('branchForm').addEventListener('submit', function(e) {
e.preventDefault();
validateBranch();
});
document.getElementById('branchForm').addEventListener('submit', function (e) {
e.preventDefault();
validateBranch();
});
function validateBranch() {
const branchName = document.getElementById('branch_name').value.trim();
if (!branchName) {
alert('Please enter a branch name');
return;
}
// Show loading state
const validation = document.getElementById('branchValidation');
const message = document.getElementById('validationMessage');
const validateBtn = document.getElementById('validateBtn');
const nextBtn = document.getElementById('nextBtn');
validation.className = 'branch-validation loading';
validation.style.display = 'block';
message.textContent = 'Validating branch...';
validateBtn.disabled = true;
nextBtn.style.display = 'none';
// Make AJAX request
fetch('/jobs/submit/step1', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `branch_name=${encodeURIComponent(branchName)}`
})
.then(response => response.json())
.then(data => {
console.log('Branch validation response:', data); // Debug log
if (data.success) {
// Success
validation.className = 'branch-validation success';
message.textContent = data.message;
validateBtn.style.display = 'none';
nextBtn.style.display = 'inline-block';
validatedBranch = branchName;
organizedData = data.organized_data || {};
scenarioMap = data.scenario_map || {};
// Log debug info
if (data.debug) {
console.log('Debug info:', data.debug);
}
if (data.output) {
console.log('Command output:', data.output);
}
} else {
// Error
validation.className = 'branch-validation error';
message.textContent = data.error;
validateBtn.disabled = false;
nextBtn.style.display = 'none';
validatedBranch = null;
organizedData = {};
scenarioMap = {};
// Log debug info for troubleshooting
console.error('Branch validation failed:', data.error);
if (data.debug) {
console.error('Debug info:', data.debug);
}
if (data.output) {
console.error('Command output:', data.output);
}
function validateBranch() {
const branchName = document.getElementById('branch_name').value.trim();
if (!branchName) {
alert('Please enter a branch name');
return;
}
})
.catch(error => {
// Network error
validation.className = 'branch-validation error';
message.textContent = 'Network error. Please try again.';
// Show loading state
const validation = document.getElementById('branchValidation');
const message = document.getElementById('validationMessage');
const validateBtn = document.getElementById('validateBtn');
const nextBtn = document.getElementById('nextBtn');
validation.className = 'branch-validation loading';
validation.style.display = 'block';
message.textContent = 'Validating branch...';
validateBtn.disabled = true;
nextBtn.style.display = 'none';
// Make AJAX request
fetch('/jobs/submit/step1', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `branch_name=${encodeURIComponent(branchName)}`
})
.then(response => response.json())
.then(data => {
console.log('Branch validation response:', data); // Debug log
if (data.success) {
// Success
validation.className = 'branch-validation success';
message.textContent = data.message;
validateBtn.style.display = 'none';
nextBtn.style.display = 'inline-block';
validatedBranch = branchName;
organizedData = data.organized_data || {};
scenarioMap = data.scenario_map || {};
// Log debug info
if (data.debug) {
console.log('Debug info:', data.debug);
}
if (data.output) {
console.log('Command output:', data.output);
}
} else {
// Error
validation.className = 'branch-validation error';
message.textContent = data.error;
validateBtn.disabled = false;
nextBtn.style.display = 'none';
validatedBranch = null;
organizedData = {};
scenarioMap = {};
// Log debug info for troubleshooting
console.error('Branch validation failed:', data.error);
if (data.debug) {
console.error('Debug info:', data.debug);
}
if (data.output) {
console.error('Command output:', data.output);
}
}
})
.catch(error => {
// Network error
validation.className = 'branch-validation error';
message.textContent = 'Network error. Please try again.';
validateBtn.disabled = false;
nextBtn.style.display = 'none';
validatedBranch = null;
availableScenarios = [];
});
}
function proceedToStep2() {
if (!validatedBranch || Object.keys(organizedData).length === 0) {
alert('Please validate the branch first');
return;
}
// Create form to submit to step 2
const form = document.createElement('form');
form.method = 'POST';
form.action = '/jobs/submit/step2-validated';
const branchInput = document.createElement('input');
branchInput.type = 'hidden';
branchInput.name = 'branch_name';
branchInput.value = validatedBranch;
form.appendChild(branchInput);
const organizedDataInput = document.createElement('input');
organizedDataInput.type = 'hidden';
organizedDataInput.name = 'organized_data';
organizedDataInput.value = JSON.stringify(organizedData);
form.appendChild(organizedDataInput);
const scenarioMapInput = document.createElement('input');
scenarioMapInput.type = 'hidden';
scenarioMapInput.name = 'scenario_map';
scenarioMapInput.value = JSON.stringify(scenarioMap);
form.appendChild(scenarioMapInput);
document.body.appendChild(form);
form.submit();
}
// Reset validation when branch name changes
document.getElementById('branch_name').addEventListener('input', function () {
const validation = document.getElementById('branchValidation');
const validateBtn = document.getElementById('validateBtn');
const nextBtn = document.getElementById('nextBtn');
validation.style.display = 'none';
validateBtn.style.display = 'inline-block';
validateBtn.disabled = false;
nextBtn.style.display = 'none';
validatedBranch = null;
availableScenarios = [];
organizedData = {};
scenarioMap = {};
});
}
function proceedToStep2() {
if (!validatedBranch || Object.keys(organizedData).length === 0) {
alert('Please validate the branch first');
return;
}
// Create form to submit to step 2
const form = document.createElement('form');
form.method = 'POST';
form.action = '/jobs/submit/step2-validated';
const branchInput = document.createElement('input');
branchInput.type = 'hidden';
branchInput.name = 'branch_name';
branchInput.value = validatedBranch;
form.appendChild(branchInput);
const organizedDataInput = document.createElement('input');
organizedDataInput.type = 'hidden';
organizedDataInput.name = 'organized_data';
organizedDataInput.value = JSON.stringify(organizedData);
form.appendChild(organizedDataInput);
const scenarioMapInput = document.createElement('input');
scenarioMapInput.type = 'hidden';
scenarioMapInput.name = 'scenario_map';
scenarioMapInput.value = JSON.stringify(scenarioMap);
form.appendChild(scenarioMapInput);
document.body.appendChild(form);
form.submit();
}
// Reset validation when branch name changes
document.getElementById('branch_name').addEventListener('input', function() {
const validation = document.getElementById('branchValidation');
const validateBtn = document.getElementById('validateBtn');
const nextBtn = document.getElementById('nextBtn');
validation.style.display = 'none';
validateBtn.style.display = 'inline-block';
validateBtn.disabled = false;
nextBtn.style.display = 'none';
validatedBranch = null;
organizedData = {};
scenarioMap = {};
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}Review & Submit - ASF TestArena{% endblock %}
{% block content %}
<div class="submit-container">
<h2 style="margin-bottom: 30px; color: var(--dark);">Review & Submit</h2>
<div class="step-indicator">
<div class="step completed">
<div class="step-number"></div>
<div class="step-label">Branch</div>
</div>
<div class="step completed">
<div class="step-number"></div>
<div class="step-label">Scenarios</div>
</div>
<div class="step active">
<div class="step-number">3</div>
<div class="step-label">Review</div>
</div>
</div>
<div style="background: white; border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 30px;">
<div class="detail-row">
<div class="detail-label">Branch:</div>
<div class="detail-value">{{ branch_name }}</div>
</div>
<div class="detail-row">
<div class="detail-label">Scenarios:</div>
<div class="detail-value">
<strong>{{ scenarios|length }} scenarios selected</strong>
<ul style="margin-top: 10px; padding-left: 20px; max-height: 200px; overflow-y: auto;">
{% for scenario in scenarios %}
<li style="font-size: 13px; color: #4b5563; margin-bottom: 4px;">{{ scenario }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<form method="POST" action="{{ url_for('jobs.submit_final') }}">
<input type="hidden" name="branch_name" value="{{ branch_name }}">
<input type="hidden" name="scenarios" value="{{ scenarios|tojson }}">
<input type="hidden" name="scenario_map" value="{{ scenario_map|tojson }}">
<!-- Default values for deactivated sections -->
<input type="hidden" name="environment" value="staging">
<input type="hidden" name="test_mode" value="simulator">
<div class="form-actions">
<button type="button" class="btn" style="background: #6b7280; color: white;" onclick="history.back()">Back</button>
<button type="submit" class="btn btn-success" style="padding: 12px 40px;">Submit Job</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="submit-container">
<h2 style="margin-bottom: 30px; color: var(--dark);">Select Test Scenarios</h2>
<div class="step-indicator">
<div class="step completed">
<div class="step-number"></div>
@@ -17,95 +17,98 @@
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-label">Environment</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-label">Test Mode</div>
</div>
<div class="step">
<div class="step-number">5</div>
<div class="step-label">Review</div>
</div>
</div>
<div style="background: #eff6ff; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<strong>Branch:</strong> {{ branch_name }}
</div>
<form method="POST" action="{{ url_for('jobs.submit_step2') }}" id="scenarioForm">
<input type="hidden" name="branch_name" value="{{ branch_name }}">
<input type="hidden" name="scenario_map" value="{{ scenario_map|tojson }}">
<input type="hidden" name="selected_scenarios" id="selectedScenariosInput">
<div class="scenario-controls">
<button type="button" class="btn btn-sm" onclick="selectAll()" style="background: var(--success); color: white; margin-right: 10px;">Select All</button>
<button type="button" class="btn btn-sm" onclick="deselectAll()" style="background: var(--danger); color: white;">Deselect All</button>
<button type="button" class="btn btn-sm" onclick="selectAll()"
style="background: var(--success); color: white; margin-right: 10px;">Select All</button>
<button type="button" class="btn btn-sm" onclick="deselectAll()"
style="background: var(--danger); color: white;">Deselect All</button>
<span id="selectionCount" style="margin-left: 20px; font-weight: 600;">0 scenarios selected</span>
</div>
<div class="scenario-tree">
{% if organized_data %}
{% for layer_name, layer_data in organized_data.items() %}
<div class="tree-layer">
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
<span class="tree-toggle" id="toggle-{{ layer_name }}"></span>
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}" onchange="toggleLayerSelection('{{ layer_name }}')">
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', ' ').title() }}</label>
</div>
<div class="tree-children" id="children-{{ layer_name }}" style="display: none;">
{% if layer_data %}
{% for stack_name, scenarios in layer_data.items() %}
<div class="tree-stack">
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
<span class="tree-toggle" id="toggle-{{ layer_name }}-{{ stack_name }}"></span>
<input type="checkbox" class="stack-checkbox" id="stack-{{ layer_name }}-{{ stack_name }}" onchange="toggleStackSelection('{{ layer_name }}', '{{ stack_name }}')">
<label for="stack-{{ layer_name }}-{{ stack_name }}" class="tree-label stack-label">{{ stack_name.replace('_', ' ').title() }}</label>
</div>
<div class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}" style="display: none;">
{% if scenarios %}
{% for scenario in scenarios %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<span class="tree-spacer"></span>
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}" value="{{ scenario }}" onchange="updateSelectionCount()" data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario }}</label>
</div>
</div>
{% endfor %}
{% else %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<span class="tree-spacer"></span>
<span class="tree-label scenario-label" style="color: #9ca3af;">No scenarios found</span>
</div>
</div>
{% endif %}
{% for layer_name, layer_data in organized_data.items() %}
<div class="tree-layer">
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
<span class="tree-toggle" id="toggle-{{ layer_name }}"></span>
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}"
onchange="toggleLayerSelection('{{ layer_name }}')">
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', '
').title() }}</label>
</div>
<div class="tree-children" id="children-{{ layer_name }}" style="display: none;">
{% if layer_data %}
{% for stack_name, scenarios in layer_data.items() %}
<div class="tree-stack">
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
<span class="tree-toggle" id="toggle-{{ layer_name }}-{{ stack_name }}"></span>
<input type="checkbox" class="stack-checkbox" id="stack-{{ layer_name }}-{{ stack_name }}"
onchange="toggleStackSelection('{{ layer_name }}', '{{ stack_name }}')">
<label for="stack-{{ layer_name }}-{{ stack_name }}" class="tree-label stack-label">{{
stack_name.replace('_', ' ').title() }}</label>
</div>
<div class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}"
style="display: none;">
{% if scenarios %}
{% for scenario in scenarios %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<span class="tree-spacer"></span>
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}"
value="{{ scenario }}" onchange="updateSelectionCount()"
data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario
}}</label>
</div>
</div>
{% endfor %}
{% else %}
<div class="tree-stack">
<div class="tree-node stack-node">
{% else %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<span class="tree-spacer"></span>
<span class="tree-label stack-label" style="color: #9ca3af;">No stacks found</span>
<span class="tree-label scenario-label" style="color: #9ca3af;">No scenarios
found</span>
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="tree-stack">
<div class="tree-node stack-node">
<span class="tree-spacer"></span>
<span class="tree-label stack-label" style="color: #9ca3af;">No stacks found</span>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="tree-layer">
<div class="tree-node layer-node">
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
</div>
<div class="tree-layer">
<div class="tree-node layer-node">
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
</div>
</div>
{% endif %}
</div>
<div class="form-actions">
<a href="{{ url_for('jobs.submit') }}" class="btn" style="background: #6b7280; color: white;">Back</a>
<button type="submit" class="btn btn-primary" id="nextBtn" disabled>Next</button>
@@ -114,155 +117,155 @@
</div>
<script>
let selectedScenarios = new Set();
let selectedScenarios = new Set();
function toggleLayer(layerName) {
const children = document.getElementById(`children-${layerName}`);
const toggle = document.getElementById(`toggle-${layerName}`);
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
function toggleLayer(layerName) {
const children = document.getElementById(`children-${layerName}`);
const toggle = document.getElementById(`toggle-${layerName}`);
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
}
}
}
function toggleStack(layerName, stackName) {
const children = document.getElementById(`children-${layerName}-${stackName}`);
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
function toggleStack(layerName, stackName) {
const children = document.getElementById(`children-${layerName}-${stackName}`);
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
}
}
}
function toggleLayerSelection(layerName) {
const layerCheckbox = document.getElementById(`layer-${layerName}`);
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
// Update all stacks and scenarios in this layer
stackCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
checkbox.indeterminate = false;
});
scenarioCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
if (layerCheckbox.checked) {
function toggleLayerSelection(layerName) {
const layerCheckbox = document.getElementById(`layer-${layerName}`);
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
// Update all stacks and scenarios in this layer
stackCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
checkbox.indeterminate = false;
});
scenarioCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
if (layerCheckbox.checked) {
selectedScenarios.add(checkbox.value);
} else {
selectedScenarios.delete(checkbox.value);
}
});
updateSelectionCount();
}
function toggleStackSelection(layerName, stackName) {
const stackCheckbox = document.getElementById(`stack-${layerName}-${stackName}`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
// Update all scenarios in this stack
scenarioCheckboxes.forEach(checkbox => {
checkbox.checked = stackCheckbox.checked;
if (stackCheckbox.checked) {
selectedScenarios.add(checkbox.value);
} else {
selectedScenarios.delete(checkbox.value);
}
});
updateLayerState(layerName);
updateSelectionCount();
}
function updateLayerState(layerName) {
const layerCheckbox = document.getElementById(`layer-${layerName}`);
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
// Update stack states
stackCheckboxes.forEach(stackCheckbox => {
const stackName = stackCheckbox.id.replace(`stack-${layerName}-`, '');
const stackScenarios = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
const checkedCount = Array.from(stackScenarios).filter(cb => cb.checked).length;
if (checkedCount === 0) {
stackCheckbox.checked = false;
stackCheckbox.indeterminate = false;
} else if (checkedCount === stackScenarios.length) {
stackCheckbox.checked = true;
stackCheckbox.indeterminate = false;
} else {
stackCheckbox.checked = false;
stackCheckbox.indeterminate = true;
}
});
// Update layer state
const checkedScenarios = Array.from(scenarioCheckboxes).filter(cb => cb.checked).length;
if (checkedScenarios === 0) {
layerCheckbox.checked = false;
layerCheckbox.indeterminate = false;
} else if (checkedScenarios === scenarioCheckboxes.length) {
layerCheckbox.checked = true;
layerCheckbox.indeterminate = false;
} else {
layerCheckbox.checked = false;
layerCheckbox.indeterminate = true;
}
}
function updateSelectionCount() {
// Update selected scenarios set
selectedScenarios.clear();
document.querySelectorAll('.scenario-checkbox:checked').forEach(checkbox => {
selectedScenarios.add(checkbox.value);
} else {
selectedScenarios.delete(checkbox.value);
}
});
updateSelectionCount();
}
});
function toggleStackSelection(layerName, stackName) {
const stackCheckbox = document.getElementById(`stack-${layerName}-${stackName}`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
// Update all scenarios in this stack
scenarioCheckboxes.forEach(checkbox => {
checkbox.checked = stackCheckbox.checked;
if (stackCheckbox.checked) {
selectedScenarios.add(checkbox.value);
} else {
selectedScenarios.delete(checkbox.value);
}
});
updateLayerState(layerName);
updateSelectionCount();
}
// Update all layer states
const layers = new Set();
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
layers.add(checkbox.dataset.layer);
});
layers.forEach(layer => updateLayerState(layer));
function updateLayerState(layerName) {
const layerCheckbox = document.getElementById(`layer-${layerName}`);
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
// Update stack states
stackCheckboxes.forEach(stackCheckbox => {
const stackName = stackCheckbox.id.replace(`stack-${layerName}-`, '');
const stackScenarios = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
const checkedCount = Array.from(stackScenarios).filter(cb => cb.checked).length;
if (checkedCount === 0) {
stackCheckbox.checked = false;
stackCheckbox.indeterminate = false;
} else if (checkedCount === stackScenarios.length) {
stackCheckbox.checked = true;
stackCheckbox.indeterminate = false;
} else {
stackCheckbox.checked = false;
stackCheckbox.indeterminate = true;
}
});
// Update layer state
const checkedScenarios = Array.from(scenarioCheckboxes).filter(cb => cb.checked).length;
if (checkedScenarios === 0) {
layerCheckbox.checked = false;
layerCheckbox.indeterminate = false;
} else if (checkedScenarios === scenarioCheckboxes.length) {
layerCheckbox.checked = true;
layerCheckbox.indeterminate = false;
} else {
layerCheckbox.checked = false;
layerCheckbox.indeterminate = true;
// Update UI
const count = selectedScenarios.size;
document.getElementById('selectionCount').textContent = `${count} scenario${count !== 1 ? 's' : ''} selected`;
document.getElementById('nextBtn').disabled = count === 0;
// Update hidden input
document.getElementById('selectedScenariosInput').value = JSON.stringify(Array.from(selectedScenarios));
}
}
function updateSelectionCount() {
// Update selected scenarios set
selectedScenarios.clear();
document.querySelectorAll('.scenario-checkbox:checked').forEach(checkbox => {
selectedScenarios.add(checkbox.value);
});
// Update all layer states
const layers = new Set();
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
layers.add(checkbox.dataset.layer);
});
layers.forEach(layer => updateLayerState(layer));
// Update UI
const count = selectedScenarios.size;
document.getElementById('selectionCount').textContent = `${count} scenario${count !== 1 ? 's' : ''} selected`;
document.getElementById('nextBtn').disabled = count === 0;
// Update hidden input
document.getElementById('selectedScenariosInput').value = JSON.stringify(Array.from(selectedScenarios));
}
function selectAll() {
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
checkbox.checked = true;
selectedScenarios.add(checkbox.value);
});
updateSelectionCount();
}
function selectAll() {
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
checkbox.checked = true;
selectedScenarios.add(checkbox.value);
});
updateSelectionCount();
}
function deselectAll() {
document.querySelectorAll('.scenario-checkbox, .stack-checkbox, .layer-checkbox').forEach(checkbox => {
checkbox.checked = false;
checkbox.indeterminate = false;
});
selectedScenarios.clear();
updateSelectionCount();
}
function deselectAll() {
document.querySelectorAll('.scenario-checkbox, .stack-checkbox, .layer-checkbox').forEach(checkbox => {
checkbox.checked = false;
checkbox.indeterminate = false;
// Initialize
document.addEventListener('DOMContentLoaded', function () {
updateSelectionCount();
});
selectedScenarios.clear();
updateSelectionCount();
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
updateSelectionCount();
});
</script>
{% endblock %}
{% endblock %}