Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,8 @@
### Metrics (optional)
# Enable Prometheus metrics endpoint at /metrics
#ENABLE_METRICS_ENDPOINT=True

### Combined jobs view (optional)
# Maximum number of jobs that can be selected for the combined view.
# Defaults to 20 if not set.
#MAX_COMBINED_JOBS=20
5 changes: 4 additions & 1 deletion lufa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ def set_security_headers(response):
@app.context_processor
def inject_constants():
"""Inject constants"""
return {"RELOAD_INTERVAL": app.config.get("RELOAD_INTERVAL", 5000)}
return {
"RELOAD_INTERVAL": app.config.get("RELOAD_INTERVAL", 5000),
"MAX_COMBINED_JOBS": app.config.get("MAX_COMBINED_JOBS", 20),
}

@login_manager.user_loader
def load_user(
Expand Down
104 changes: 103 additions & 1 deletion lufa/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,32 @@
from lufa import render_template
from lufa.decorators import debug_only
from lufa.provider import get_backend_repository
from lufa.repository.backend_repository import ResourceNotFoundError
from lufa.repository.backend_repository import BadRequestError, ResourceNotFoundError

bp = Blueprint("frontend", __name__, url_prefix="")

DEFAULT_MAX_COMBINED_JOBS = 20


def _parse_job_ids() -> list[int]:
"""Parse the comma-separated 'ids' query param into a capped list of job ids."""
max_combined_jobs = current_app.config.get("MAX_COMBINED_JOBS", DEFAULT_MAX_COMBINED_JOBS)
raw = request.args.get("ids", "")
job_ids: list[int] = []
for part in raw.split(","):
part = part.strip()
if not part:
continue
try:
job_ids.append(int(part))
except ValueError:
continue
if len(job_ids) > max_combined_jobs:
raise BadRequestError(f"Too many jobs selected (max {max_combined_jobs})")
if not job_ids:
raise BadRequestError("No job ids provided")
return job_ids


@bp.errorhandler(ResourceNotFoundError)
def handle_resource_not_found(e: ResourceNotFoundError) -> Tuple[str, int]:
Expand All @@ -21,6 +43,13 @@ def handle_resource_not_found(e: ResourceNotFoundError) -> Tuple[str, int]:
return render_template("error.html", error_title="Resource not found", error_message=e.msg), 404


@bp.errorhandler(BadRequestError)
def handle_bad_request(e: BadRequestError) -> Tuple[str, int]:
"""Handles BadRequestError by rendering an error page with appropriate title and message."""

return render_template("error.html", error_title="Bad request", error_message=e.msg), 400


@bp.route("/", methods=["GET"])
@login_required
def welcome():
Expand Down Expand Up @@ -136,6 +165,79 @@ def job_status_data(tower_job_id: int):
return jsonify(data)


@bp.route("/jobs/combined", methods=["GET"])
@bp.route("/jobs/combined/overview", methods=["GET"])
@login_required
def job_combined_overview():
"""Renders the multi-job combined overview page."""
job_ids = _parse_job_ids()
repo = get_backend_repository()
return render_template(
"job_combined_overview.html",
job_ids=job_ids,
jobs_by_id=repo.get_jobs_by_ids(job_ids),
)


@bp.route("/data/jobs/combined/overview", methods=["GET"])
@login_required
def job_combined_overview_data():
"""Returns the data for the multi-job combined overview page in json format."""
job_ids = _parse_job_ids()
repo = get_backend_repository()
data = {
"hosts_table": {"data": repo.get_last_host_callback_by_ids(job_ids)},
"host_states": repo.get_last_host_callback_count_by_ids(job_ids),
}
return jsonify(data)


@bp.route("/jobs/combined/recap", methods=["GET"])
@login_required
def job_combined_recap():
"""Renders the multi-job combined recap page."""
job_ids = _parse_job_ids()
return render_template(
"job_combined_recap.html",
job_ids=job_ids,
jobs_by_id=get_backend_repository().get_jobs_by_ids(job_ids),
)


@bp.route("/data/jobs/combined/recap", methods=["GET"])
@login_required
def job_combined_recap_data():
"""Returns the data for the multi-job combined recap page in json format."""
job_ids = _parse_job_ids()
data = {"stats_table": {"data": get_backend_repository().get_job_stats_by_ids(job_ids)}}
return jsonify(data)


@bp.route("/jobs/combined/callbacks", methods=["GET"])
@login_required
def job_combined_callbacks():
"""Renders the multi-job combined callbacks page."""
job_ids = _parse_job_ids()
return render_template(
"job_combined_callbacks.html",
job_ids=job_ids,
jobs_by_id=get_backend_repository().get_jobs_by_ids(job_ids),
)


@bp.route("/data/jobs/combined/callbacks", methods=["GET"])
@login_required
def job_combined_callbacks_data():
"""Returns the data for the multi-job combined callbacks page in json format."""
job_ids = _parse_job_ids()
repo = get_backend_repository()
data = {
"callbacks_table": {"data": repo.get_job_task_callbacks_by_ids(job_ids)},
"task_states": repo.get_job_task_callback_count_by_ids(job_ids),
}
return jsonify(data)


@bp.route("/jobs/<int:tower_job_id>/infos", methods=["GET"])
@login_required
def job_infos(tower_job_id: int):
Expand Down
Loading
Loading