filtry v jobs a shrnuti
This commit is contained in:
+43
-3
@@ -246,14 +246,34 @@ def get_next_queued_job():
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def get_jobs(limit: int = 100):
|
def get_jobs(
|
||||||
|
limit: int = 100,
|
||||||
|
status: str | None = None,
|
||||||
|
job_type: str | None = None,
|
||||||
|
target: str | None = None,
|
||||||
|
):
|
||||||
run_migrations()
|
run_migrations()
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
|
|
||||||
|
filters = []
|
||||||
|
params: list[Any] = []
|
||||||
|
if status:
|
||||||
|
filters.append("status = ?")
|
||||||
|
params.append(status)
|
||||||
|
if job_type:
|
||||||
|
filters.append("type = ?")
|
||||||
|
params.append(job_type)
|
||||||
|
if target:
|
||||||
|
filters.append("(target_type LIKE ? OR target_id LIKE ?)")
|
||||||
|
target_pattern = f"%{target}%"
|
||||||
|
params.extend([target_pattern, target_pattern])
|
||||||
|
|
||||||
|
where_sql = f"WHERE {' AND '.join(filters)}" if filters else ""
|
||||||
rows = con.execute(
|
rows = con.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM jobs
|
FROM jobs
|
||||||
|
{where_sql}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN status = 'running' THEN 0
|
WHEN status = 'running' THEN 0
|
||||||
@@ -263,13 +283,33 @@ def get_jobs(limit: int = 100):
|
|||||||
id DESC
|
id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
(limit,),
|
(*params, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
con.close()
|
con.close()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_job_stats():
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
rows = con.execute(
|
||||||
|
"""
|
||||||
|
SELECT status, COUNT(*) AS count
|
||||||
|
FROM jobs
|
||||||
|
WHERE status IN ('queued', 'running', 'failed', 'success')
|
||||||
|
GROUP BY status
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
stats = {"queued": 0, "running": 0, "failed": 0, "success": 0}
|
||||||
|
for row in rows:
|
||||||
|
stats[row["status"]] = row["count"]
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
def get_job(job_id: int):
|
def get_job(job_id: int):
|
||||||
run_migrations()
|
run_migrations()
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
|
|||||||
+56
-4
@@ -2,12 +2,12 @@ import html
|
|||||||
import json
|
import json
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from app.auth import require_user
|
from app.auth import require_user
|
||||||
from app.db.audit import log_audit_event
|
from app.db.audit import log_audit_event
|
||||||
from app.db.jobs import cancel_job, get_job, get_job_logs, get_jobs, retry_failed_job
|
from app.db.jobs import cancel_job, get_job, get_job_logs, get_job_stats, get_jobs, retry_failed_job
|
||||||
from app.routes.deployments import calculate_duration, render_status_pill
|
from app.routes.deployments import calculate_duration, render_status_pill
|
||||||
from app.templates.layout import page
|
from app.templates.layout import page
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ router = APIRouter()
|
|||||||
|
|
||||||
LIVE_STATUSES = {"queued", "running", "cancelled_requested"}
|
LIVE_STATUSES = {"queued", "running", "cancelled_requested"}
|
||||||
JOB_STATUSES = {"queued", "running", "cancelled_requested", "cancelled", "success", "failed"}
|
JOB_STATUSES = {"queued", "running", "cancelled_requested", "cancelled", "success", "failed"}
|
||||||
|
JOB_FILTER_STATUSES = ("queued", "running", "success", "failed", "cancelled")
|
||||||
|
JOB_FILTER_TYPES = ("deploy_app", "deploy_core_service")
|
||||||
|
|
||||||
|
|
||||||
def render_job_status(status: str | None) -> str:
|
def render_job_status(status: str | None) -> str:
|
||||||
@@ -45,9 +47,37 @@ def pretty_json(value: str | None) -> str:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def render_options(values: tuple[str, ...], selected: str, empty_label: str) -> str:
|
||||||
|
options = [f'<option value="">{html.escape(empty_label)}</option>']
|
||||||
|
for value in values:
|
||||||
|
selected_attr = " selected" if selected == value else ""
|
||||||
|
escaped_value = html.escape(value)
|
||||||
|
options.append(f'<option value="{escaped_value}"{selected_attr}>{escaped_value}</option>')
|
||||||
|
return "".join(options)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/jobs", response_class=HTMLResponse)
|
@router.get("/jobs", response_class=HTMLResponse)
|
||||||
def jobs_page(request: Request, user=Depends(require_user)):
|
def jobs_page(
|
||||||
jobs = get_jobs()
|
request: Request,
|
||||||
|
status: str = Query(""),
|
||||||
|
job_type: str = Query("", alias="type"),
|
||||||
|
target: str = Query(""),
|
||||||
|
user=Depends(require_user),
|
||||||
|
):
|
||||||
|
selected_status = status.strip().lower()
|
||||||
|
selected_type = job_type.strip()
|
||||||
|
selected_target = target.strip()
|
||||||
|
if selected_status not in JOB_FILTER_STATUSES:
|
||||||
|
selected_status = ""
|
||||||
|
if selected_type not in JOB_FILTER_TYPES:
|
||||||
|
selected_type = ""
|
||||||
|
|
||||||
|
jobs = get_jobs(
|
||||||
|
status=selected_status or None,
|
||||||
|
job_type=selected_type or None,
|
||||||
|
target=selected_target or None,
|
||||||
|
)
|
||||||
|
stats = get_job_stats()
|
||||||
refresh = "<script>setTimeout(() => window.location.reload(), 5000);</script>" if any(
|
refresh = "<script>setTimeout(() => window.location.reload(), 5000);</script>" if any(
|
||||||
(job.get("status") or "").lower() in LIVE_STATUSES for job in jobs
|
(job.get("status") or "").lower() in LIVE_STATUSES for job in jobs
|
||||||
) else ""
|
) else ""
|
||||||
@@ -86,6 +116,10 @@ def jobs_page(request: Request, user=Depends(require_user)):
|
|||||||
if not rows:
|
if not rows:
|
||||||
rows = '<tr><td colspan="9">Zatím nejsou evidované žádné joby.</td></tr>'
|
rows = '<tr><td colspan="9">Zatím nejsou evidované žádné joby.</td></tr>'
|
||||||
|
|
||||||
|
status_options = render_options(JOB_FILTER_STATUSES, selected_status, "Všechny statusy")
|
||||||
|
type_options = render_options(JOB_FILTER_TYPES, selected_type, "Všechny typy")
|
||||||
|
target_value = html.escape(selected_target)
|
||||||
|
|
||||||
return page(
|
return page(
|
||||||
"Joby",
|
"Joby",
|
||||||
f"""
|
f"""
|
||||||
@@ -95,6 +129,24 @@ def jobs_page(request: Request, user=Depends(require_user)):
|
|||||||
<p class="muted">Fronta portálových a webhook úloh připravená pro centrální worker.</p>
|
<p class="muted">Fronta portálových a webhook úloh připravená pro centrální worker.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card stat-warning"><span>Queued</span><strong>{stats.get("queued") or 0}</strong></div>
|
||||||
|
<div class="stat-card stat-warning"><span>Running</span><strong>{stats.get("running") or 0}</strong></div>
|
||||||
|
<div class="stat-card stat-danger"><span>Failed</span><strong>{stats.get("failed") or 0}</strong></div>
|
||||||
|
<div class="stat-card stat-success"><span>Success</span><strong>{stats.get("success") or 0}</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Filtry</h2>
|
||||||
|
<form method="get" action="/portal/jobs" class="filter-form">
|
||||||
|
<select name="status">{status_options}</select>
|
||||||
|
<select name="type">{type_options}</select>
|
||||||
|
<input name="target" value="{target_value}" placeholder="Target">
|
||||||
|
<button type="submit">Filtrovat</button>
|
||||||
|
<a class="btn btn-secondary" href="/portal/jobs">Reset</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Fronta</h2>
|
<h2>Fronta</h2>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
Reference in New Issue
Block a user