new testarena

This commit is contained in:
2025-11-28 11:22:07 +01:00
parent 22f7f2f94d
commit fb26b8386b
48 changed files with 7105 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
import os
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///testarena.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
from app.models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# Register blueprints
from app.routes.auth import auth_bp
from app.routes.admin import admin_bp
from app.routes.dashboard import dashboard_bp
from app.routes.jobs import jobs_bp
app.register_blueprint(auth_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(jobs_bp)
with app.app_context():
db.create_all()
# Create default admin user if not exists
if not User.query.filter_by(username='admin').first():
admin = User(username='admin', is_admin=True)
admin.set_password('admin123')
db.session.add(admin)
db.session.commit()
return app

View File

@@ -0,0 +1,47 @@
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
jobs = db.relationship('Job', backref='submitter', lazy=True, cascade='all, delete-orphan')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Job(db.Model):
__tablename__ = 'jobs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
branch_name = db.Column(db.String(255), nullable=False)
scenarios = db.Column(db.Text, nullable=False) # JSON string
environment = db.Column(db.String(50), nullable=False)
test_mode = db.Column(db.String(50), nullable=False)
status = db.Column(db.String(20), default='in_progress') # in_progress, passed, failed, aborted
submitted_at = db.Column(db.DateTime, default=datetime.utcnow)
completed_at = db.Column(db.DateTime, nullable=True)
duration = db.Column(db.Integer, nullable=True) # in seconds
keep_devbenches = db.Column(db.Boolean, default=False)
reuse_results = db.Column(db.Boolean, default=False)
results_path = db.Column(db.String(500), nullable=True)
def get_status_icon(self):
icons = {
'in_progress': '🟠',
'passed': '🟢',
'failed': '🔴',
'aborted': ''
}
return icons.get(self.status, '')

View File

@@ -0,0 +1,81 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from app.models import User
from app import db
from functools import wraps
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
flash('Access denied. Admin privileges required.', 'error')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@login_required
@admin_required
def index():
users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin/dashboard.html', users=users)
@admin_bp.route('/users/create', methods=['POST'])
@login_required
@admin_required
def create_user():
username = request.form.get('username')
password = request.form.get('password')
is_admin = request.form.get('is_admin') == 'on'
if not username or not password:
flash('Username and password are required', 'error')
return redirect(url_for('admin.index'))
if User.query.filter_by(username=username).first():
flash('Username already exists', 'error')
return redirect(url_for('admin.index'))
user = User(username=username, is_admin=is_admin)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash(f'User {username} created successfully', 'success')
return redirect(url_for('admin.index'))
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
@admin_required
def reset_password(user_id):
user = User.query.get_or_404(user_id)
new_password = request.form.get('new_password')
if not new_password:
flash('New password is required', 'error')
return redirect(url_for('admin.index'))
user.set_password(new_password)
db.session.commit()
flash(f'Password reset for {user.username}', 'success')
return redirect(url_for('admin.index'))
@admin_bp.route('/users/<int:user_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete_user(user_id):
user = User.query.get_or_404(user_id)
if user.id == current_user.id:
flash('Cannot delete your own account', 'error')
return redirect(url_for('admin.index'))
username = user.username
db.session.delete(user)
db.session.commit()
flash(f'User {username} deleted successfully', 'success')
return redirect(url_for('admin.index'))

View File

@@ -0,0 +1,32 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required
from app.models import User
from app import db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/')
def index():
return redirect(url_for('auth.login'))
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
return redirect(url_for('dashboard.index'))
else:
flash('Invalid username or password', 'error')
return render_template('login.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))

View File

@@ -0,0 +1,15 @@
from flask import Blueprint, render_template
from flask_login import login_required, current_user
from app.models import Job
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
@dashboard_bp.route('/')
@login_required
def index():
if current_user.is_admin:
jobs = Job.query.order_by(Job.submitted_at.desc()).all()
else:
jobs = Job.query.filter_by(user_id=current_user.id).order_by(Job.submitted_at.desc()).all()
return render_template('dashboard/index.html', jobs=jobs)

View File

@@ -0,0 +1,123 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from app.models import Job
from app import db
import json
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('/submit/step1', methods=['POST'])
@login_required
def submit_step1():
branch_name = request.form.get('branch_name')
# TODO: Implement branch checkout and scenario detection
# For now, return mock scenarios
scenarios = [
'Scenario_1_Basic_Test',
'Scenario_2_Advanced_Test',
'Scenario_3_Integration_Test',
'Scenario_4_Performance_Test',
'Scenario_5_Security_Test'
]
return render_template('jobs/submit_step2.html', branch_name=branch_name, scenarios=scenarios)
@jobs_bp.route('/submit/step2', methods=['POST'])
@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 = request.form.get('scenarios')
environment = request.form.get('environment')
return render_template('jobs/submit_step4.html',
branch_name=branch_name,
scenarios=scenarios,
environment=environment)
@jobs_bp.route('/submit/final', methods=['POST'])
@login_required
def submit_final():
branch_name = request.form.get('branch_name')
scenarios = request.form.get('scenarios')
environment = request.form.get('environment')
test_mode = request.form.get('test_mode')
keep_devbenches = request.form.get('keep_devbenches') == 'on'
reuse_results = request.form.get('reuse_results') == 'on'
job = Job(
user_id=current_user.id,
branch_name=branch_name,
scenarios=scenarios,
environment=environment,
test_mode=test_mode,
keep_devbenches=keep_devbenches,
reuse_results=reuse_results,
status='in_progress'
)
db.session.add(job)
db.session.commit()
# TODO: Start test execution in background
flash('Test job submitted successfully', 'success')
return redirect(url_for('dashboard.index'))
@jobs_bp.route('/<int:job_id>')
@login_required
def view_job(job_id):
job = Job.query.get_or_404(job_id)
if not current_user.is_admin and job.user_id != current_user.id:
flash('Access denied', 'error')
return redirect(url_for('dashboard.index'))
return jsonify({
'id': job.id,
'submitter': job.submitter.username,
'branch_name': job.branch_name,
'scenarios': job.scenarios,
'environment': job.environment,
'test_mode': job.test_mode,
'status': job.status,
'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,
'duration': job.duration,
'keep_devbenches': job.keep_devbenches,
'reuse_results': job.reuse_results,
'results_path': job.results_path
})
@jobs_bp.route('/<int:job_id>/abort', methods=['POST'])
@login_required
def abort_job(job_id):
job = Job.query.get_or_404(job_id)
if not current_user.is_admin and job.user_id != current_user.id:
return jsonify({'error': 'Access denied'}), 403
if job.status == 'in_progress':
job.status = 'aborted'
db.session.commit()
# TODO: Kill the running process
return jsonify({'success': True})
return jsonify({'error': 'Job is not in progress'}), 400

View File

@@ -0,0 +1,542 @@
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--dark: #1f2937;
--light: #f3f4f6;
--border: #e5e7eb;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Login Page */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.login-box {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
width: 100%;
max-width: 400px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo img {
max-width: 120px;
height: auto;
}
.logo h1 {
color: var(--dark);
margin-top: 15px;
font-size: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--dark);
font-weight: 500;
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 14px;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: var(--primary);
color: white;
width: 100%;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* Alert Messages */
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
/* Navbar */
.navbar {
background: white;
padding: 15px 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar-brand {
display: flex;
align-items: center;
gap: 15px;
}
.navbar-brand img {
height: 40px;
}
.navbar-brand h2 {
color: var(--dark);
font-size: 20px;
}
.navbar-menu {
display: flex;
gap: 20px;
align-items: center;
}
.navbar-menu a {
color: var(--dark);
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
}
.navbar-menu a:hover {
color: var(--primary);
}
/* Dashboard Layout */
.dashboard-container {
display: grid;
grid-template-columns: 400px 1fr;
gap: 20px;
margin-top: 20px;
height: calc(100vh - 120px);
}
.panel {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow-y: auto;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid var(--border);
}
.panel-header h3 {
color: var(--dark);
font-size: 18px;
}
/* Job List */
.job-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.job-item {
padding: 15px;
border: 2px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 12px;
}
.job-item:hover {
border-color: var(--primary);
background: var(--light);
}
.job-item.active {
border-color: var(--primary);
background: #eff6ff;
}
.job-status-icon {
font-size: 24px;
}
.job-info h4 {
color: var(--dark);
font-size: 14px;
margin-bottom: 4px;
}
.job-info p {
color: #6b7280;
font-size: 12px;
}
/* Job Details */
.job-details {
display: none;
}
.job-details.active {
display: block;
}
.detail-row {
display: grid;
grid-template-columns: 150px 1fr;
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.detail-label {
font-weight: 600;
color: var(--dark);
}
.detail-value {
color: #4b5563;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.status-in_progress {
background: #fef3c7;
color: #92400e;
}
.status-passed {
background: #d1fae5;
color: #065f46;
}
.status-failed {
background: #fee2e2;
color: #991b1b;
}
.status-aborted {
background: #f3f4f6;
color: #374151;
}
/* Admin Dashboard */
.admin-container {
background: white;
border-radius: 12px;
padding: 30px;
margin-top: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.admin-header h2 {
color: var(--dark);
}
.user-table {
width: 100%;
border-collapse: collapse;
}
.user-table th {
background: var(--light);
padding: 12px;
text-align: left;
font-weight: 600;
color: var(--dark);
border-bottom: 2px solid var(--border);
}
.user-table td {
padding: 12px;
border-bottom: 1px solid var(--border);
}
.user-table tr:hover {
background: var(--light);
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.badge-admin {
background: #dbeafe;
color: #1e40af;
}
.badge-user {
background: #f3f4f6;
color: #374151;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.modal.active {
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 12px;
width: 90%;
max-width: 500px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h3 {
color: var(--dark);
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6b7280;
}
/* Submit Form */
.submit-container {
background: white;
border-radius: 12px;
padding: 40px;
margin-top: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
}
.step {
flex: 1;
text-align: center;
position: relative;
}
.step::after {
content: '';
position: absolute;
top: 15px;
left: 50%;
width: 100%;
height: 2px;
background: var(--border);
z-index: -1;
}
.step:last-child::after {
display: none;
}
.step.active .step-number {
background: var(--primary);
color: white;
}
.step.completed .step-number {
background: var(--success);
color: white;
}
.step-number {
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--border);
color: var(--dark);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-bottom: 8px;
}
.step-label {
font-size: 12px;
color: #6b7280;
}
.checkbox-group {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin: 20px 0;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0;
}
.radio-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
border: 2px solid var(--border);
border-radius: 8px;
cursor: pointer;
}
.radio-item:hover {
border-color: var(--primary);
}
.radio-item input[type="radio"]:checked + label {
color: var(--primary);
font-weight: 600;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 30px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6b7280;
}
.empty-state h3 {
margin-bottom: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block title %}Admin Dashboard - ASF TestArena{% endblock %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h2>User Management</h2>
<button class="btn btn-primary" onclick="openModal('createUserModal')">+ Create User</button>
</div>
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Role</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
{% if user.is_admin %}
<span class="badge badge-admin">Admin</span>
{% else %}
<span class="badge badge-user">User</span>
{% endif %}
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="openResetPasswordModal({{ user.id }}, '{{ user.username }}')">Reset Password</button>
{% if user.id != current_user.id %}
<form method="POST" action="{{ url_for('admin.delete_user', user_id=user.id) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete user {{ user.username }}?')">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Create User Modal -->
<div id="createUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Create New User</h3>
<button class="close-btn" onclick="closeModal('createUserModal')">&times;</button>
</div>
<form method="POST" action="{{ url_for('admin.create_user') }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_admin">
Admin User
</label>
</div>
<button type="submit" class="btn btn-primary">Create User</button>
</form>
</div>
</div>
<!-- Reset Password Modal -->
<div id="resetPasswordModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Reset Password</h3>
<button class="close-btn" onclick="closeModal('resetPasswordModal')">&times;</button>
</div>
<form id="resetPasswordForm" method="POST">
<p>Reset password for: <strong id="resetUsername"></strong></p>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Reset Password</button>
</form>
</div>
</div>
<script>
function openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
function openResetPasswordModal(userId, username) {
document.getElementById('resetUsername').textContent = username;
document.getElementById('resetPasswordForm').action = `/admin/users/${userId}/reset-password`;
openModal('resetPasswordModal');
}
// Close modal when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.classList.remove('active');
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}ASF TestArena{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
{% if current_user.is_authenticated %}
<nav class="navbar">
<div class="navbar-brand">
<img src="{{ url_for('static', filename='uploads/icon.png') }}" alt="Logo">
<h2>ASF TestArena</h2>
</div>
<div class="navbar-menu">
<a href="{{ url_for('dashboard.index') }}">Dashboard</a>
<a href="{{ url_for('jobs.submit') }}">Submit Job</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.index') }}">Admin</a>
{% endif %}
<span>{{ current_user.username }}</span>
<a href="{{ url_for('auth.logout') }}">Logout</a>
</div>
</nav>
{% endif %}
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block title %}Dashboard - ASF TestArena{% endblock %}
{% block content %}
<div class="dashboard-container">
<div class="panel">
<div class="panel-header">
<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 }})">
<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>
</div>
{% endfor %}
{% else %}
<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>
<p>Click on a job from the list to view details</p>
</div>
</div>
</div>
</div>
<script>
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 = `
<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>
<div class="detail-row">
<div class="detail-label">Branch:</div>
<div class="detail-value">${job.branch_name}</div>
</div>
<div class="detail-row">
<div class="detail-label">Status:</div>
<div class="detail-value">
<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>
` : ''}
`;
});
}
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 %}

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}Submit Test Job - ASF TestArena{% endblock %}
{% 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>
<div class="step-label">Branch</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-label">Scenarios</div>
</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 method="POST" action="{{ url_for('jobs.submit_step1') }}">
<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>
<small style="color: #6b7280; margin-top: 5px; display: block;">
Enter the branch name to analyze available test scenarios
</small>
</div>
<div class="form-actions">
<a href="{{ url_for('dashboard.index') }}" class="btn" style="background: #6b7280; color: white;">Cancel</a>
<button type="submit" class="btn btn-primary">Next</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Select Scenarios - ASF TestArena{% endblock %}
{% 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>
<div class="step-label">Branch</div>
</div>
<div class="step active">
<div class="step-number">2</div>
<div class="step-label">Scenarios</div>
</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') }}">
<input type="hidden" name="branch_name" value="{{ branch_name }}">
<div class="form-group">
<label>
<input type="checkbox" id="selectAll" onclick="toggleAll(this)">
Select All Scenarios
</label>
</div>
<div class="checkbox-group">
{% for scenario in scenarios %}
<div class="checkbox-item">
<input type="checkbox" name="scenarios" value="{{ scenario }}" id="scenario_{{ loop.index }}">
<label for="scenario_{{ loop.index }}">{{ scenario }}</label>
</div>
{% endfor %}
</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">Next</button>
</div>
</form>
</div>
<script>
function toggleAll(checkbox) {
const checkboxes = document.querySelectorAll('input[name="scenarios"]');
checkboxes.forEach(cb => cb.checked = checkbox.checked);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}Select Environment - ASF TestArena{% endblock %}
{% block content %}
<div class="submit-container">
<h2 style="margin-bottom: 30px; color: var(--dark);">Select Environment</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">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 method="POST" action="{{ url_for('jobs.submit_step3') }}">
<input type="hidden" name="branch_name" value="{{ branch_name }}">
<input type="hidden" name="scenarios" value="{{ scenarios|tojson }}">
<div class="radio-group">
<label class="radio-item">
<input type="radio" name="environment" value="sensor_hub" required>
<div>
<strong>Sensor Hub</strong>
<p style="font-size: 12px; color: #6b7280; margin-top: 4px;">Test on sensor hub hardware/simulator</p>
</div>
</label>
<label class="radio-item">
<input type="radio" name="environment" value="main_board" required>
<div>
<strong>Main Board</strong>
<p style="font-size: 12px; color: #6b7280; margin-top: 4px;">Test on main board hardware/simulator</p>
</div>
</label>
</div>
<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-primary">Next</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}Select Test Mode - ASF TestArena{% endblock %}
{% block content %}
<div class="submit-container">
<h2 style="margin-bottom: 30px; color: var(--dark);">Select Test Mode</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 completed">
<div class="step-number"></div>
<div class="step-label">Environment</div>
</div>
<div class="step active">
<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 method="POST" action="{{ url_for('jobs.submit_final') }}">
<input type="hidden" name="branch_name" value="{{ branch_name }}">
<input type="hidden" name="scenarios" value="{{ scenarios }}">
<input type="hidden" name="environment" value="{{ environment }}">
<div class="radio-group">
<label class="radio-item">
<input type="radio" name="test_mode" value="devbench_simulator" required>
<div>
<strong>Devbench Simulator</strong>
<p style="font-size: 12px; color: #6b7280; margin-top: 4px;">Fully simulated environment</p>
</div>
</label>
<label class="radio-item">
<input type="radio" name="test_mode" value="testbench_hil" required>
<div>
<strong>Testbench HIL</strong>
<p style="font-size: 12px; color: #6b7280; margin-top: 4px;">Hardware-in-the-Loop with real devices</p>
</div>
</label>
</div>
<div class="form-group" style="margin-top: 30px;">
<h3 style="margin-bottom: 15px; color: var(--dark);">Additional Options</h3>
<div style="margin-bottom: 10px;">
<label>
<input type="checkbox" name="keep_devbenches">
Keep devbenches after test completion
</label>
</div>
<div>
<label>
<input type="checkbox" name="reuse_results">
Reuse previous test results if available
</label>
</div>
</div>
<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">Start Test</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - ASF TestArena</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div class="login-container">
<div class="login-box">
<div class="logo">
<img src="{{ url_for('static', filename='uploads/icon.png') }}" alt="Logo">
<h1>ASF TestArena</h1>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
</body>
</html>