next_updates
This commit is contained in:
@@ -37,11 +37,18 @@ class Job(db.Model):
|
|||||||
reuse_results = db.Column(db.Boolean, default=False)
|
reuse_results = db.Column(db.Boolean, default=False)
|
||||||
results_path = db.Column(db.String(500), nullable=True)
|
results_path = db.Column(db.String(500), nullable=True)
|
||||||
|
|
||||||
|
# Phase 2 Remote Tracking
|
||||||
|
remote_queue_id = db.Column(db.String(50), nullable=True)
|
||||||
|
remote_task_ids = db.Column(db.Text, nullable=True) # JSON string: {scenario_name: task_id}
|
||||||
|
remote_results = db.Column(db.Text, nullable=True) # JSON string: {scenario_name: [status, link]}
|
||||||
|
queue_log = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
def get_status_icon(self):
|
def get_status_icon(self):
|
||||||
icons = {
|
icons = {
|
||||||
'in_progress': '🟠',
|
'waiting': '⌛',
|
||||||
'passed': '🟢',
|
'in_progress': '🔄',
|
||||||
'failed': '🔴',
|
'passed': '✅',
|
||||||
|
'failed': '❌',
|
||||||
'aborted': '⚫'
|
'aborted': '⚫'
|
||||||
}
|
}
|
||||||
return icons.get(self.status, '⚪')
|
return icons.get(self.status, '⚪')
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ from app import db
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
def generate_remote_id(length=6):
|
||||||
|
return ''.join(random.choices(string.digits, k=length))
|
||||||
|
|
||||||
jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs')
|
jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs')
|
||||||
|
|
||||||
@jobs_bp.route('/submit')
|
|
||||||
@login_required
|
|
||||||
def submit():
|
|
||||||
return render_template('jobs/submit.html')
|
|
||||||
|
|
||||||
@jobs_bp.route('/debug/test-step2')
|
@jobs_bp.route('/debug/test-step2')
|
||||||
@login_required
|
@login_required
|
||||||
def debug_test_step2():
|
def debug_test_step2():
|
||||||
@@ -270,7 +271,7 @@ def submit_step2():
|
|||||||
flash('Please select at least one scenario', 'error')
|
flash('Please select at least one scenario', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
|
|
||||||
return render_template('jobs/submit_step3.html',
|
return render_template('jobs/submit_review.html',
|
||||||
branch_name=branch_name,
|
branch_name=branch_name,
|
||||||
scenarios=selected_scenarios,
|
scenarios=selected_scenarios,
|
||||||
scenario_map=scenario_map)
|
scenario_map=scenario_map)
|
||||||
@@ -318,50 +319,14 @@ def submit_step2_validated():
|
|||||||
print(f"[ERROR] Step2 - Traceback: {traceback.format_exc()}")
|
print(f"[ERROR] Step2 - Traceback: {traceback.format_exc()}")
|
||||||
flash(f'Error loading scenario selection: {str(e)}', 'error')
|
flash(f'Error loading scenario selection: {str(e)}', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
@login_required
|
|
||||||
def submit_step2():
|
|
||||||
branch_name = request.form.get('branch_name')
|
|
||||||
selected_scenarios = request.form.getlist('scenarios')
|
|
||||||
|
|
||||||
if not selected_scenarios:
|
|
||||||
flash('Please select at least one scenario', 'error')
|
|
||||||
return redirect(url_for('jobs.submit'))
|
|
||||||
|
|
||||||
return render_template('jobs/submit_step3.html',
|
|
||||||
branch_name=branch_name,
|
|
||||||
scenarios=selected_scenarios)
|
|
||||||
|
|
||||||
@jobs_bp.route('/submit/step3', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def submit_step3():
|
|
||||||
branch_name = request.form.get('branch_name')
|
|
||||||
scenarios_json = request.form.get('scenarios')
|
|
||||||
scenario_map_json = request.form.get('scenario_map')
|
|
||||||
environment = request.form.get('environment')
|
|
||||||
|
|
||||||
try:
|
|
||||||
scenarios = json.loads(scenarios_json) if scenarios_json else []
|
|
||||||
scenario_map = json.loads(scenario_map_json) if scenario_map_json else {}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
flash('Invalid scenario data', 'error')
|
|
||||||
return redirect(url_for('jobs.submit'))
|
|
||||||
|
|
||||||
return render_template('jobs/submit_step4.html',
|
|
||||||
branch_name=branch_name,
|
|
||||||
scenarios=scenarios,
|
|
||||||
scenario_map=scenario_map,
|
|
||||||
environment=environment)
|
|
||||||
|
|
||||||
@jobs_bp.route('/submit/final', methods=['POST'])
|
@jobs_bp.route('/submit/final', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def submit_final():
|
def submit_final():
|
||||||
branch_name = request.form.get('branch_name')
|
branch_name = request.form.get('branch_name')
|
||||||
scenarios_json = request.form.get('scenarios')
|
scenarios_json = request.form.get('scenarios')
|
||||||
scenario_map_json = request.form.get('scenario_map')
|
scenario_map_json = request.form.get('scenario_map')
|
||||||
environment = request.form.get('environment')
|
environment = request.form.get('environment', 'staging')
|
||||||
test_mode = request.form.get('test_mode')
|
test_mode = request.form.get('test_mode', 'simulator')
|
||||||
keep_devbenches = request.form.get('keep_devbenches') == 'on'
|
|
||||||
reuse_results = request.form.get('reuse_results') == 'on'
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scenarios = json.loads(scenarios_json) if scenarios_json else []
|
scenarios = json.loads(scenarios_json) if scenarios_json else []
|
||||||
@@ -370,22 +335,44 @@ def submit_final():
|
|||||||
flash('Invalid scenario data', 'error')
|
flash('Invalid scenario data', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
|
|
||||||
|
# Generate Remote IDs
|
||||||
|
remote_queue_id = generate_remote_id()
|
||||||
|
remote_task_ids = {s: generate_remote_id() for s in scenarios}
|
||||||
|
|
||||||
|
# Prepare Remote Payload
|
||||||
|
# Format: {"source": "branch", "queue_id": ["staging", {"task_id": "path"}]}
|
||||||
|
payload = {
|
||||||
|
"source": branch_name,
|
||||||
|
remote_queue_id: [
|
||||||
|
environment,
|
||||||
|
{remote_task_ids[s]: scenario_map.get(s, s) for s in scenarios}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trigger Remote Queue
|
||||||
|
try:
|
||||||
|
remote_url = "http://asf-server.duckdns.org:8080/api/queue"
|
||||||
|
response = requests.post(remote_url, json=payload, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
print(f"[DEBUG] Remote queue triggered: {response.json()}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to trigger remote queue: {e}")
|
||||||
|
flash(f'Warning: Job saved but failed to trigger remote queue: {str(e)}', 'warning')
|
||||||
|
|
||||||
job = Job(
|
job = Job(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
branch_name=branch_name,
|
branch_name=branch_name,
|
||||||
scenarios=json.dumps(scenarios), # Store as JSON string
|
scenarios=json.dumps(scenarios),
|
||||||
environment=environment,
|
environment=environment,
|
||||||
test_mode=test_mode,
|
test_mode=test_mode,
|
||||||
keep_devbenches=keep_devbenches,
|
status='waiting', # Start with waiting (sand watch)
|
||||||
reuse_results=reuse_results,
|
remote_queue_id=remote_queue_id,
|
||||||
status='in_progress'
|
remote_task_ids=json.dumps(remote_task_ids)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(job)
|
db.session.add(job)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# TODO: Start test execution in background using scenario_map
|
|
||||||
|
|
||||||
flash('Test job submitted successfully', 'success')
|
flash('Test job submitted successfully', 'success')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
|
|
||||||
@@ -409,23 +396,96 @@ def view_job(job_id):
|
|||||||
'submitted_at': job.submitted_at.strftime('%Y-%m-%d %H:%M:%S'),
|
'submitted_at': job.submitted_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
'completed_at': job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else None,
|
'completed_at': job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else None,
|
||||||
'duration': job.duration,
|
'duration': job.duration,
|
||||||
'keep_devbenches': job.keep_devbenches,
|
'remote_queue_id': job.remote_queue_id,
|
||||||
'reuse_results': job.reuse_results,
|
'remote_task_ids': job.remote_task_ids,
|
||||||
'results_path': job.results_path
|
'remote_results': job.remote_results,
|
||||||
|
'queue_log': job.queue_log
|
||||||
})
|
})
|
||||||
|
|
||||||
@jobs_bp.route('/<int:job_id>/abort', methods=['POST'])
|
@jobs_bp.route('/<int:job_id>/abort', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def abort_job(job_id):
|
def abort_job(job_id):
|
||||||
|
return jsonify({'error': 'Abort functionality is not implemented yet'}), 400
|
||||||
|
|
||||||
|
@jobs_bp.route('/<int:job_id>/status')
|
||||||
|
@login_required
|
||||||
|
def get_job_status(job_id):
|
||||||
job = Job.query.get_or_404(job_id)
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
if not current_user.is_admin and job.user_id != current_user.id:
|
if job.status not in ['passed', 'failed', 'aborted']:
|
||||||
return jsonify({'error': 'Access denied'}), 403
|
# Poll remote API
|
||||||
|
try:
|
||||||
if job.status == 'in_progress':
|
# 1. Check Queue Status
|
||||||
job.status = 'aborted'
|
status_url = f"http://asf-server.duckdns.org:8080/api/status/{job.remote_queue_id}"
|
||||||
db.session.commit()
|
q_resp = requests.get(status_url, timeout=5)
|
||||||
# TODO: Kill the running process
|
if q_resp.status_code == 200:
|
||||||
return jsonify({'success': True})
|
q_data = q_resp.json()
|
||||||
|
remote_status = q_data.get('status', '').lower()
|
||||||
return jsonify({'error': 'Job is not in progress'}), 400
|
|
||||||
|
if remote_status == 'running':
|
||||||
|
job.status = 'in_progress'
|
||||||
|
elif remote_status == 'waiting':
|
||||||
|
job.status = 'waiting'
|
||||||
|
|
||||||
|
# 2. Fetch Queue Logs if running
|
||||||
|
if remote_status == 'running':
|
||||||
|
log_url = f"http://asf-server.duckdns.org:8080/results/{job.remote_queue_id}/queue_log.txt"
|
||||||
|
l_resp = requests.get(log_url, timeout=5)
|
||||||
|
if l_resp.status_code == 200:
|
||||||
|
job.queue_log = l_resp.text
|
||||||
|
|
||||||
|
# 3. Check Task Statuses and Results
|
||||||
|
task_ids = json.loads(job.remote_task_ids) if job.remote_task_ids else {}
|
||||||
|
results = json.loads(job.remote_results) if job.remote_results else {}
|
||||||
|
|
||||||
|
all_finished = True
|
||||||
|
any_failed = False
|
||||||
|
|
||||||
|
for scenario, task_id in task_ids.items():
|
||||||
|
# If we don't have result yet, check it
|
||||||
|
if scenario not in results:
|
||||||
|
# Check task status
|
||||||
|
t_status_url = f"http://asf-server.duckdns.org:8080/api/status/{task_id}"
|
||||||
|
t_resp = requests.get(t_status_url, timeout=2)
|
||||||
|
if t_resp.status_code == 200:
|
||||||
|
t_data = t_resp.json()
|
||||||
|
if t_data.get('status') == 'Finished':
|
||||||
|
# Fetch results
|
||||||
|
res_url = f"http://asf-server.duckdns.org:8080/results/{job.remote_queue_id}/{task_id}/final_summary.json"
|
||||||
|
r_resp = requests.get(res_url, timeout=2)
|
||||||
|
if r_resp.status_code == 200:
|
||||||
|
r_data = r_resp.json()
|
||||||
|
# User says: {"SCENARIO_NAME": ["PASS/FAIL", "link"]}
|
||||||
|
# We need to find the key that matches our scenario (case insensitive maybe?)
|
||||||
|
for key, val in r_data.items():
|
||||||
|
if key.upper() == scenario.upper():
|
||||||
|
results[scenario] = val
|
||||||
|
if val[0] == 'FAIL':
|
||||||
|
any_failed = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
all_finished = False
|
||||||
|
else:
|
||||||
|
all_finished = False
|
||||||
|
else:
|
||||||
|
all_finished = False
|
||||||
|
else:
|
||||||
|
if results[scenario][0] == 'FAIL':
|
||||||
|
any_failed = True
|
||||||
|
|
||||||
|
if all_finished and task_ids:
|
||||||
|
job.status = 'failed' if any_failed else 'passed'
|
||||||
|
job.completed_at = db.session.query(db.func.now()).scalar()
|
||||||
|
|
||||||
|
job.remote_results = json.dumps(results)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Status polling failed: {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': job.status,
|
||||||
|
'status_icon': job.get_status_icon(),
|
||||||
|
'remote_results': job.remote_results,
|
||||||
|
'queue_log': job.queue_log
|
||||||
|
})
|
||||||
|
|||||||
@@ -623,8 +623,8 @@ body {
|
|||||||
background: #9ca3af;
|
background: #9ca3af;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}/
|
}
|
||||||
* Scenario Tree Styles */
|
/* Scenario Tree Styles */
|
||||||
.scenario-controls {
|
.scenario-controls {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@@ -753,4 +753,34 @@ input[type="checkbox"]:indeterminate::before {
|
|||||||
#selectionCount {
|
#selectionCount {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scenario Table & Log Styles */
|
||||||
|
#scenarioTable {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scenarioTable th {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scenarioTable td {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue-log {
|
||||||
|
border: 1px solid #333;
|
||||||
|
box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#scenarioSearch:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-waiting {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,32 +9,33 @@
|
|||||||
<h3>Test Jobs</h3>
|
<h3>Test Jobs</h3>
|
||||||
<a href="{{ url_for('jobs.submit') }}" class="btn btn-primary btn-sm">+ New Job</a>
|
<a href="{{ url_for('jobs.submit') }}" class="btn btn-primary btn-sm">+ New Job</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="job-list">
|
<div class="job-list">
|
||||||
{% if jobs %}
|
{% if jobs %}
|
||||||
{% for job in 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-item" data-job-id="{{ job.id }}" onclick="loadJobDetails({{ job.id }})"
|
||||||
<div class="job-status-icon">{{ job.get_status_icon() }}</div>
|
oncontextmenu="showContextMenu(event, {{ job.id }}, '{{ job.status }}')">
|
||||||
<div class="job-info">
|
<div class="job-status-icon">{{ job.get_status_icon() }}</div>
|
||||||
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
|
<div class="job-info">
|
||||||
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
|
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
|
||||||
</div>
|
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No jobs yet</h3>
|
<h3>No jobs yet</h3>
|
||||||
<p>Submit your first test job to get started</p>
|
<p>Submit your first test job to get started</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Job Details</h3>
|
<h3>Job Details</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="job-details-container">
|
<div id="job-details-container">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>Select a job</h3>
|
<h3>Select a job</h3>
|
||||||
@@ -53,61 +54,90 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let contextJobId = null;
|
let contextJobId = null;
|
||||||
|
|
||||||
function showContextMenu(event, jobId, status) {
|
function showContextMenu(event, jobId, status) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Only show context menu for in_progress jobs
|
// Only show context menu for in_progress jobs
|
||||||
if (status !== 'in_progress') {
|
if (status !== 'in_progress') {
|
||||||
return;
|
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() {
|
function hideContextMenu() {
|
||||||
document.getElementById('contextMenu').style.display = 'none';
|
document.getElementById('contextMenu').style.display = 'none';
|
||||||
document.removeEventListener('click', hideContextMenu);
|
document.removeEventListener('click', hideContextMenu);
|
||||||
contextJobId = null;
|
contextJobId = null;
|
||||||
}
|
|
||||||
|
|
||||||
function abortJobFromContext() {
|
|
||||||
if (contextJobId) {
|
|
||||||
abortJob(contextJobId);
|
|
||||||
}
|
}
|
||||||
hideContextMenu();
|
|
||||||
}
|
function abortJobFromContext() {
|
||||||
function loadJobDetails(jobId) {
|
if (contextJobId) {
|
||||||
// Mark job as active
|
abortJob(contextJobId);
|
||||||
document.querySelectorAll('.job-item').forEach(item => {
|
}
|
||||||
item.classList.remove('active');
|
hideContextMenu();
|
||||||
});
|
}
|
||||||
document.querySelector(`[data-job-id="${jobId}"]`).classList.add('active');
|
let pollingInterval = null;
|
||||||
|
|
||||||
// Fetch job details
|
function loadJobDetails(jobId) {
|
||||||
fetch(`/jobs/${jobId}`)
|
// Clear existing polling
|
||||||
.then(response => response.json())
|
if (pollingInterval) {
|
||||||
.then(job => {
|
clearInterval(pollingInterval);
|
||||||
const container = document.getElementById('job-details-container');
|
pollingInterval = null;
|
||||||
const scenarios = JSON.parse(job.scenarios || '[]');
|
}
|
||||||
|
|
||||||
container.innerHTML = `
|
// 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-row">
|
||||||
<div class="detail-label">Job ID:</div>
|
<div class="detail-label">Job ID:</div>
|
||||||
<div class="detail-value">#${job.id}</div>
|
<div class="detail-value">#${job.id} (Remote: ${job.remote_queue_id || 'N/A'})</div>
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Submitter:</div>
|
|
||||||
<div class="detail-value">${job.submitter}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">Branch:</div>
|
<div class="detail-label">Branch:</div>
|
||||||
@@ -115,62 +145,114 @@ function loadJobDetails(jobId) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">Status:</div>
|
<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>
|
<span class="status-badge status-${job.status}">${job.status.replace('_', ' ').toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Environment:</div>
|
<div style="margin-top: 30px;">
|
||||||
<div class="detail-value">${job.environment}</div>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||||
</div>
|
<h4 style="color: var(--dark);">Scenario Summary</h4>
|
||||||
<div class="detail-row">
|
<input type="text" id="scenarioSearch" placeholder="Search scenarios..." onkeyup="filterScenarios()" class="form-control" style="width: 200px; padding: 6px 12px;">
|
||||||
<div class="detail-label">Test Mode:</div>
|
</div>
|
||||||
<div class="detail-value">${job.test_mode}</div>
|
<table class="user-table" id="scenarioTable">
|
||||||
</div>
|
<thead>
|
||||||
<div class="detail-row">
|
<tr>
|
||||||
<div class="detail-label">Submitted:</div>
|
<th>Scenario Name</th>
|
||||||
<div class="detail-value">${job.submitted_at}</div>
|
<th>Task ID</th>
|
||||||
</div>
|
<th>Status</th>
|
||||||
${job.completed_at ? `
|
</tr>
|
||||||
<div class="detail-row">
|
</thead>
|
||||||
<div class="detail-label">Completed:</div>
|
<tbody>
|
||||||
<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>
|
|
||||||
` : ''}
|
|
||||||
`;
|
`;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortJob(jobId) {
|
scenarios.forEach(s => {
|
||||||
if (confirm('Are you sure you want to abort this job?')) {
|
const taskId = taskIds[s] || 'N/A';
|
||||||
fetch(`/jobs/${jobId}/abort`, { method: 'POST' })
|
const result = results[s]; // [status, link]
|
||||||
.then(response => response.json())
|
let statusHtml = '⌛ Waiting';
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
if (result) {
|
||||||
location.reload();
|
const icon = result[0] === 'PASS' ? '✅' : '❌';
|
||||||
} else {
|
const color = result[0] === 'PASS' ? 'var(--success)' : 'var(--danger)';
|
||||||
alert(data.error);
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="submit-container">
|
<div class="submit-container">
|
||||||
<h2 style="margin-bottom: 30px; color: var(--dark);">Submit New Test Job</h2>
|
<h2 style="margin-bottom: 30px; color: var(--dark);">Submit New Test Job</h2>
|
||||||
|
|
||||||
<div class="step-indicator">
|
<div class="step-indicator">
|
||||||
<div class="step active">
|
<div class="step active">
|
||||||
<div class="step-number">1</div>
|
<div class="step-number">1</div>
|
||||||
@@ -17,181 +17,174 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-number">3</div>
|
<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 class="step-label">Review</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="branchForm">
|
<form id="branchForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="branch_name">Git Branch Name</label>
|
<label for="branch_name">Git Branch Name</label>
|
||||||
<input type="text" id="branch_name" name="branch_name" class="form-control"
|
<input type="text" id="branch_name" name="branch_name" class="form-control"
|
||||||
placeholder="e.g., feature/new-feature" required>
|
placeholder="e.g., feature/new-feature" required>
|
||||||
<small style="color: #6b7280; margin-top: 5px; display: block;">
|
<small style="color: #6b7280; margin-top: 5px; display: block;">
|
||||||
Enter the branch name to analyze available test scenarios
|
Enter the branch name to analyze available test scenarios
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div id="branchValidation" class="branch-validation">
|
<div id="branchValidation" class="branch-validation">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
<span id="validationMessage">Validating branch...</span>
|
<span id="validationMessage">Validating branch...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a href="{{ url_for('dashboard.index') }}" class="btn" style="background: #6b7280; color: white;">Cancel</a>
|
<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="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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let validatedBranch = null;
|
let validatedBranch = null;
|
||||||
let organizedData = {};
|
let organizedData = {};
|
||||||
let scenarioMap = {};
|
let scenarioMap = {};
|
||||||
|
|
||||||
document.getElementById('branchForm').addEventListener('submit', function(e) {
|
document.getElementById('branchForm').addEventListener('submit', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
validateBranch();
|
validateBranch();
|
||||||
});
|
});
|
||||||
|
|
||||||
function validateBranch() {
|
function validateBranch() {
|
||||||
const branchName = document.getElementById('branch_name').value.trim();
|
const branchName = document.getElementById('branch_name').value.trim();
|
||||||
|
|
||||||
if (!branchName) {
|
if (!branchName) {
|
||||||
alert('Please enter a branch name');
|
alert('Please enter a branch name');
|
||||||
return;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
// Show loading state
|
||||||
// Network error
|
const validation = document.getElementById('branchValidation');
|
||||||
validation.className = 'branch-validation error';
|
const message = document.getElementById('validationMessage');
|
||||||
message.textContent = 'Network error. Please try again.';
|
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;
|
validateBtn.disabled = false;
|
||||||
nextBtn.style.display = 'none';
|
nextBtn.style.display = 'none';
|
||||||
|
|
||||||
validatedBranch = null;
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
57
app/templates/jobs/submit_review.html
Normal file
57
app/templates/jobs/submit_review.html
Normal 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 %}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="submit-container">
|
<div class="submit-container">
|
||||||
<h2 style="margin-bottom: 30px; color: var(--dark);">Select Test Scenarios</h2>
|
<h2 style="margin-bottom: 30px; color: var(--dark);">Select Test Scenarios</h2>
|
||||||
|
|
||||||
<div class="step-indicator">
|
<div class="step-indicator">
|
||||||
<div class="step completed">
|
<div class="step completed">
|
||||||
<div class="step-number">✓</div>
|
<div class="step-number">✓</div>
|
||||||
@@ -17,95 +17,98 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-number">3</div>
|
<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 class="step-label">Review</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background: #eff6ff; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
<div style="background: #eff6ff; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||||
<strong>Branch:</strong> {{ branch_name }}
|
<strong>Branch:</strong> {{ branch_name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('jobs.submit_step2') }}" id="scenarioForm">
|
<form method="POST" action="{{ url_for('jobs.submit_step2') }}" id="scenarioForm">
|
||||||
<input type="hidden" name="branch_name" value="{{ branch_name }}">
|
<input type="hidden" name="branch_name" value="{{ branch_name }}">
|
||||||
<input type="hidden" name="scenario_map" value="{{ scenario_map|tojson }}">
|
<input type="hidden" name="scenario_map" value="{{ scenario_map|tojson }}">
|
||||||
<input type="hidden" name="selected_scenarios" id="selectedScenariosInput">
|
<input type="hidden" name="selected_scenarios" id="selectedScenariosInput">
|
||||||
|
|
||||||
<div class="scenario-controls">
|
<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="selectAll()"
|
||||||
<button type="button" class="btn btn-sm" onclick="deselectAll()" style="background: var(--danger); color: white;">Deselect All</button>
|
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>
|
<span id="selectionCount" style="margin-left: 20px; font-weight: 600;">0 scenarios selected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scenario-tree">
|
<div class="scenario-tree">
|
||||||
{% if organized_data %}
|
{% if organized_data %}
|
||||||
{% for layer_name, layer_data in organized_data.items() %}
|
{% for layer_name, layer_data in organized_data.items() %}
|
||||||
<div class="tree-layer">
|
<div class="tree-layer">
|
||||||
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
|
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
|
||||||
<span class="tree-toggle" id="toggle-{{ layer_name }}">▶</span>
|
<span class="tree-toggle" id="toggle-{{ layer_name }}">▶</span>
|
||||||
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}" onchange="toggleLayerSelection('{{ layer_name }}')">
|
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}"
|
||||||
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', ' ').title() }}</label>
|
onchange="toggleLayerSelection('{{ layer_name }}')">
|
||||||
</div>
|
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', '
|
||||||
|
').title() }}</label>
|
||||||
<div class="tree-children" id="children-{{ layer_name }}" style="display: none;">
|
</div>
|
||||||
{% if layer_data %}
|
|
||||||
{% for stack_name, scenarios in layer_data.items() %}
|
<div class="tree-children" id="children-{{ layer_name }}" style="display: none;">
|
||||||
<div class="tree-stack">
|
{% if layer_data %}
|
||||||
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
|
{% for stack_name, scenarios in layer_data.items() %}
|
||||||
<span class="tree-toggle" id="toggle-{{ layer_name }}-{{ stack_name }}">▶</span>
|
<div class="tree-stack">
|
||||||
<input type="checkbox" class="stack-checkbox" id="stack-{{ layer_name }}-{{ stack_name }}" onchange="toggleStackSelection('{{ layer_name }}', '{{ stack_name }}')">
|
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
|
||||||
<label for="stack-{{ layer_name }}-{{ stack_name }}" class="tree-label stack-label">{{ stack_name.replace('_', ' ').title() }}</label>
|
<span class="tree-toggle" id="toggle-{{ layer_name }}-{{ stack_name }}">▶</span>
|
||||||
</div>
|
<input type="checkbox" class="stack-checkbox" id="stack-{{ layer_name }}-{{ stack_name }}"
|
||||||
|
onchange="toggleStackSelection('{{ layer_name }}', '{{ stack_name }}')">
|
||||||
<div class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}" style="display: none;">
|
<label for="stack-{{ layer_name }}-{{ stack_name }}" class="tree-label stack-label">{{
|
||||||
{% if scenarios %}
|
stack_name.replace('_', ' ').title() }}</label>
|
||||||
{% for scenario in scenarios %}
|
</div>
|
||||||
<div class="tree-scenario">
|
|
||||||
<div class="tree-node scenario-node">
|
<div class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}"
|
||||||
<span class="tree-spacer"></span>
|
style="display: none;">
|
||||||
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}" value="{{ scenario }}" onchange="updateSelectionCount()" data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
|
{% if scenarios %}
|
||||||
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario }}</label>
|
{% for scenario in scenarios %}
|
||||||
</div>
|
<div class="tree-scenario">
|
||||||
</div>
|
<div class="tree-node scenario-node">
|
||||||
{% endfor %}
|
<span class="tree-spacer"></span>
|
||||||
{% else %}
|
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}"
|
||||||
<div class="tree-scenario">
|
value="{{ scenario }}" onchange="updateSelectionCount()"
|
||||||
<div class="tree-node scenario-node">
|
data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
|
||||||
<span class="tree-spacer"></span>
|
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario
|
||||||
<span class="tree-label scenario-label" style="color: #9ca3af;">No scenarios found</span>
|
}}</label>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tree-stack">
|
<div class="tree-scenario">
|
||||||
<div class="tree-node stack-node">
|
<div class="tree-node scenario-node">
|
||||||
<span class="tree-spacer"></span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tree-layer">
|
<div class="tree-layer">
|
||||||
<div class="tree-node layer-node">
|
<div class="tree-node layer-node">
|
||||||
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
|
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a href="{{ url_for('jobs.submit') }}" class="btn" style="background: #6b7280; color: white;">Back</a>
|
<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>
|
<button type="submit" class="btn btn-primary" id="nextBtn" disabled>Next</button>
|
||||||
@@ -114,155 +117,155 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let selectedScenarios = new Set();
|
let selectedScenarios = new Set();
|
||||||
|
|
||||||
function toggleLayer(layerName) {
|
function toggleLayer(layerName) {
|
||||||
const children = document.getElementById(`children-${layerName}`);
|
const children = document.getElementById(`children-${layerName}`);
|
||||||
const toggle = document.getElementById(`toggle-${layerName}`);
|
const toggle = document.getElementById(`toggle-${layerName}`);
|
||||||
|
|
||||||
if (children.style.display === 'none') {
|
if (children.style.display === 'none') {
|
||||||
children.style.display = 'block';
|
children.style.display = 'block';
|
||||||
toggle.textContent = '▼';
|
toggle.textContent = '▼';
|
||||||
} else {
|
} else {
|
||||||
children.style.display = 'none';
|
children.style.display = 'none';
|
||||||
toggle.textContent = '▶';
|
toggle.textContent = '▶';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStack(layerName, stackName) {
|
function toggleStack(layerName, stackName) {
|
||||||
const children = document.getElementById(`children-${layerName}-${stackName}`);
|
const children = document.getElementById(`children-${layerName}-${stackName}`);
|
||||||
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
|
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
|
||||||
|
|
||||||
if (children.style.display === 'none') {
|
if (children.style.display === 'none') {
|
||||||
children.style.display = 'block';
|
children.style.display = 'block';
|
||||||
toggle.textContent = '▼';
|
toggle.textContent = '▼';
|
||||||
} else {
|
} else {
|
||||||
children.style.display = 'none';
|
children.style.display = 'none';
|
||||||
toggle.textContent = '▶';
|
toggle.textContent = '▶';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLayerSelection(layerName) {
|
function toggleLayerSelection(layerName) {
|
||||||
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
||||||
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
||||||
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
||||||
|
|
||||||
// Update all stacks and scenarios in this layer
|
// Update all stacks and scenarios in this layer
|
||||||
stackCheckboxes.forEach(checkbox => {
|
stackCheckboxes.forEach(checkbox => {
|
||||||
checkbox.checked = layerCheckbox.checked;
|
checkbox.checked = layerCheckbox.checked;
|
||||||
checkbox.indeterminate = false;
|
checkbox.indeterminate = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
scenarioCheckboxes.forEach(checkbox => {
|
scenarioCheckboxes.forEach(checkbox => {
|
||||||
checkbox.checked = layerCheckbox.checked;
|
checkbox.checked = layerCheckbox.checked;
|
||||||
if (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);
|
selectedScenarios.add(checkbox.value);
|
||||||
} else {
|
});
|
||||||
selectedScenarios.delete(checkbox.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateSelectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStackSelection(layerName, stackName) {
|
// Update all layer states
|
||||||
const stackCheckbox = document.getElementById(`stack-${layerName}-${stackName}`);
|
const layers = new Set();
|
||||||
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
|
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
||||||
|
layers.add(checkbox.dataset.layer);
|
||||||
// Update all scenarios in this stack
|
});
|
||||||
scenarioCheckboxes.forEach(checkbox => {
|
layers.forEach(layer => updateLayerState(layer));
|
||||||
checkbox.checked = stackCheckbox.checked;
|
|
||||||
if (stackCheckbox.checked) {
|
|
||||||
selectedScenarios.add(checkbox.value);
|
|
||||||
} else {
|
|
||||||
selectedScenarios.delete(checkbox.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateLayerState(layerName);
|
|
||||||
updateSelectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLayerState(layerName) {
|
// Update UI
|
||||||
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
const count = selectedScenarios.size;
|
||||||
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
document.getElementById('selectionCount').textContent = `${count} scenario${count !== 1 ? 's' : ''} selected`;
|
||||||
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
document.getElementById('nextBtn').disabled = count === 0;
|
||||||
|
|
||||||
// Update stack states
|
// Update hidden input
|
||||||
stackCheckboxes.forEach(stackCheckbox => {
|
document.getElementById('selectedScenariosInput').value = JSON.stringify(Array.from(selectedScenarios));
|
||||||
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() {
|
function selectAll() {
|
||||||
// Update selected scenarios set
|
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
||||||
selectedScenarios.clear();
|
checkbox.checked = true;
|
||||||
document.querySelectorAll('.scenario-checkbox:checked').forEach(checkbox => {
|
selectedScenarios.add(checkbox.value);
|
||||||
selectedScenarios.add(checkbox.value);
|
});
|
||||||
});
|
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));
|
|
||||||
|
|
||||||
// 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() {
|
function deselectAll() {
|
||||||
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
document.querySelectorAll('.scenario-checkbox, .stack-checkbox, .layer-checkbox').forEach(checkbox => {
|
||||||
checkbox.checked = true;
|
checkbox.checked = false;
|
||||||
selectedScenarios.add(checkbox.value);
|
checkbox.indeterminate = false;
|
||||||
});
|
});
|
||||||
updateSelectionCount();
|
selectedScenarios.clear();
|
||||||
}
|
updateSelectionCount();
|
||||||
|
}
|
||||||
|
|
||||||
function deselectAll() {
|
// Initialize
|
||||||
document.querySelectorAll('.scenario-checkbox, .stack-checkbox, .layer-checkbox').forEach(checkbox => {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
checkbox.checked = false;
|
updateSelectionCount();
|
||||||
checkbox.indeterminate = false;
|
|
||||||
});
|
});
|
||||||
selectedScenarios.clear();
|
|
||||||
updateSelectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
updateSelectionCount();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -7,3 +7,4 @@ gunicorn==21.2.0
|
|||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
WTForms==3.1.1
|
WTForms==3.1.1
|
||||||
|
requests==2.31.0
|
||||||
|
|||||||
Reference in New Issue
Block a user