cestina, UI, UX
This commit is contained in:
+131
-33
@@ -1,7 +1,8 @@
|
||||
import html
|
||||
from urllib.parse import quote
|
||||
import math
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from ..auth import require_user
|
||||
@@ -16,17 +17,46 @@ from ..config import (
|
||||
)
|
||||
from ..db.apps import get_app, get_app_deployments, get_apps, update_app_resources
|
||||
from ..db.audit import log_audit_event
|
||||
from ..db.jobs import create_job, has_active_deploy_job
|
||||
from ..db.jobs import create_job, get_jobs, has_active_deploy_job
|
||||
from ..routes.deployments import render_status_pill
|
||||
from ..shell import run_command
|
||||
from ..templates.layout import page, render_result
|
||||
|
||||
router = APIRouter()
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request, user=Depends(require_user)):
|
||||
@router.get("/")
|
||||
def portal_home(user=Depends(require_user)):
|
||||
return RedirectResponse(url="/portal/operations", status_code=303)
|
||||
|
||||
|
||||
@router.get("/apps", response_class=HTMLResponse)
|
||||
def apps_page(
|
||||
request: Request,
|
||||
q: str = Query(""),
|
||||
status: str = Query(""),
|
||||
page_number: int = Query(1, alias="page", ge=1),
|
||||
user=Depends(require_user),
|
||||
):
|
||||
apps = get_apps()
|
||||
query = q.strip()
|
||||
selected_status = status.strip()
|
||||
if query:
|
||||
apps = [
|
||||
item for item in apps
|
||||
if query.lower() in (item.get("id", "") or "").lower()
|
||||
or query.lower() in (item.get("name", "") or "").lower()
|
||||
]
|
||||
if selected_status:
|
||||
apps = [item for item in apps if (item.get("status", "") or "") == selected_status]
|
||||
status_values = sorted({item.get("status", "") for item in get_apps() if item.get("status")})
|
||||
total_apps = len(apps)
|
||||
total_pages = max(1, math.ceil(total_apps / DEFAULT_PAGE_SIZE))
|
||||
if page_number > total_pages:
|
||||
page_number = total_pages
|
||||
offset = (page_number - 1) * DEFAULT_PAGE_SIZE
|
||||
apps = apps[offset : offset + DEFAULT_PAGE_SIZE]
|
||||
|
||||
gitea_url = read_env_value("GITEA_URL", "")
|
||||
gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
|
||||
@@ -108,8 +138,8 @@ def index(request: Request, user=Depends(require_user)):
|
||||
<td>
|
||||
<p><a class="btn" href="/portal/apps/{app_url_id}">Detail</a></p>
|
||||
<p><a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení</a></p>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit redeploy aplikace {app_id}?');">
|
||||
<button type="submit" class="btn-secondary">Redeploy</button>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit nové nasazení služby {app_id}?');">
|
||||
<button type="submit" class="btn-secondary">Nasadit znovu</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/delete-app" onsubmit="return confirm('Smazat {app_id}? Tím se odstraní kontejner, image, workspace, záznam v katalogu a Gitea repozitář.');">
|
||||
<input type="hidden" name="app_id" value="{app_id}">
|
||||
@@ -120,14 +150,40 @@ def index(request: Request, user=Depends(require_user)):
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="6">Zatím nejsou nasazené žádné aplikace.</td></tr>'
|
||||
rows = '<tr><td colspan="6">Zatím nejsou nasazené žádné služby.</td></tr>'
|
||||
status_options = ['<option value="">Všechny stavy</option>']
|
||||
for value in status_values:
|
||||
selected = " selected" if selected_status == value else ""
|
||||
escaped_value = html.escape(value)
|
||||
status_options.append(f'<option value="{escaped_value}"{selected}>{escaped_value}</option>')
|
||||
first_item = offset + 1 if total_apps else 0
|
||||
last_item = min(offset + len(apps), total_apps)
|
||||
|
||||
def page_url(page: int) -> str:
|
||||
params = {"page": page}
|
||||
if query:
|
||||
params["q"] = query
|
||||
if selected_status:
|
||||
params["status"] = selected_status
|
||||
return f"/portal/apps?{urlencode(params)}"
|
||||
|
||||
pagination = f"""
|
||||
<div class="pagination">
|
||||
<span>Zobrazeno {first_item}-{last_item} z {total_apps}</span>
|
||||
<div class="pagination-actions">
|
||||
<a class="btn btn-secondary{' disabled' if page_number <= 1 else ''}" href="{page_url(max(1, page_number - 1))}">Předchozí</a>
|
||||
<span class="page-indicator">Strana {page_number} / {total_pages}</span>
|
||||
<a class="btn btn-secondary{' disabled' if page_number >= total_pages else ''}" href="{page_url(min(total_pages, page_number + 1))}">Další</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return page(
|
||||
"Aplikace",
|
||||
"Služby",
|
||||
f"""
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Aplikace</h2>
|
||||
<h2>Služby</h2>
|
||||
<p class="muted">Vytváření, nasazení, klonování, nastavení prostředků a mazání služeb.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -143,11 +199,19 @@ def index(request: Request, user=Depends(require_user)):
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Nasazené aplikace</h2>
|
||||
<p><a class="btn" href="/portal/new-app">+ Nová aplikace</a></p>
|
||||
<h2>Nasazené služby</h2>
|
||||
<p><a class="btn" href="/portal/new-app">+ Nová služba</a></p>
|
||||
<form method="get" action="/portal/apps" class="filter-form">
|
||||
<input name="q" value="{html.escape(query)}" placeholder="Název nebo ID služby">
|
||||
<select name="status">{"".join(status_options)}</select>
|
||||
<input type="hidden" name="page" value="1">
|
||||
<button type="submit">Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/apps">Reset</a>
|
||||
</form>
|
||||
{pagination}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Aplikace</th>
|
||||
<th>Služba</th>
|
||||
<th>Status</th>
|
||||
<th>Dokumentace</th>
|
||||
<th>Prostředky</th>
|
||||
@@ -156,6 +220,7 @@ def index(request: Request, user=Depends(require_user)):
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
{pagination}
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
@@ -197,20 +262,39 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="4">Zatím nejsou evidovaná žádná nasazení této aplikace.</td></tr>'
|
||||
rows = '<tr><td colspan="4">Zatím nejsou evidovaná žádná nasazení této služby.</td></tr>'
|
||||
|
||||
job_rows = ""
|
||||
for job in get_jobs(limit=100, target=app.get("id", "")):
|
||||
if job.get("target_type") != "app" or job.get("target_id") != app.get("id", ""):
|
||||
continue
|
||||
|
||||
job_id = html.escape(str(job.get("id", "")))
|
||||
job_rows += f"""
|
||||
<tr>
|
||||
<td><a href="/portal/jobs/{job_id}">#{job_id}</a></td>
|
||||
<td>{render_status_pill(job.get("status"))}</td>
|
||||
<td>{html.escape(job.get("type", "") or "")}</td>
|
||||
<td>{html.escape(job.get("created_at", "") or "")}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not job_rows:
|
||||
job_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné úlohy této služby.</td></tr>'
|
||||
|
||||
return page(
|
||||
f"Aplikace {escaped_app_id}",
|
||||
f"Služba {escaped_app_id}",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>{escaped_app_id}</h2>
|
||||
<p class="muted">{name}</p>
|
||||
<p>
|
||||
<a class="btn" href="/portal">← Zpět na aplikace</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení aplikace</a>
|
||||
<a class="btn" href="/portal/apps">← Zpět na služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/jobs?target={app_url_id}">Úlohy služby</a>
|
||||
</p>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit redeploy aplikace {escaped_app_id}?');">
|
||||
<button type="submit">Redeploy</button>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit nové nasazení služby {escaped_app_id}?');">
|
||||
<button type="submit">Nasadit znovu</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -225,21 +309,35 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
<tr><th>Paměť</th><td>{memory}</td></tr>
|
||||
<tr><th>CPU</th><td>{cpus}</td></tr>
|
||||
<tr><th>Upraveno</th><td>{updated_at}</td></tr>
|
||||
<tr><th>Logy</th><td><a href="/portal/jobs?target={app_url_id}">Zobrazit úlohy a logy</a></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Poslední deploymenty</h2>
|
||||
<h2>Historie nasazení</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Triggered by</th>
|
||||
<th>Čas</th>
|
||||
<th>Spustil</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Historie úloh</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Typ</th>
|
||||
<th>Vytvořeno</th>
|
||||
</tr>
|
||||
{job_rows}
|
||||
</table>
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
@@ -277,20 +375,20 @@ def redeploy_app(app_id: str, user=Depends(require_user)):
|
||||
@router.get("/new-app", response_class=HTMLResponse)
|
||||
def new_app_form(request: Request, user=Depends(require_user)):
|
||||
return page(
|
||||
"Nová aplikace",
|
||||
"Nová služba",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2>Vytvořit novou aplikaci</h2>
|
||||
<h2>Vytvořit novou službu</h2>
|
||||
<p class="muted">Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.</p>
|
||||
|
||||
<form method="post" action="/portal/new-app">
|
||||
<p>
|
||||
<label>ID aplikace</label><br>
|
||||
<label>ID služby</label><br>
|
||||
<input name="app_id" placeholder="gmail-service" required>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>Název aplikace</label><br>
|
||||
<label>Název služby</label><br>
|
||||
<input name="app_name" placeholder="Gmail služba" required>
|
||||
</p>
|
||||
|
||||
@@ -301,10 +399,10 @@ def new_app_form(request: Request, user=Depends(require_user)):
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<button type="submit">Vytvořit aplikaci</button>
|
||||
<button type="submit">Vytvořit službu</button>
|
||||
</form>
|
||||
|
||||
<p><a href="/portal">← Zpět</a></p>
|
||||
<p><a href="/portal/apps">← Zpět</a></p>
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
@@ -341,8 +439,8 @@ def create_app(
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Vytvoření aplikace: {status}",
|
||||
back_url="/portal",
|
||||
title=f"Vytvoření služby: {status}",
|
||||
back_url="/portal/apps",
|
||||
sections=[
|
||||
("Výstup vytvoření", create_result.stdout),
|
||||
("Chyba vytvoření", create_result.stderr),
|
||||
@@ -372,8 +470,8 @@ def delete_app(app_id: str = Form(...), user=Depends(require_user)):
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Smazání aplikace: {status}",
|
||||
back_url="/portal",
|
||||
title=f"Smazání služby: {status}",
|
||||
back_url="/portal/apps",
|
||||
sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
|
||||
user=user,
|
||||
)
|
||||
@@ -418,7 +516,7 @@ def update_resources(
|
||||
|
||||
return render_result(
|
||||
title=f"Úprava prostředků: {status}",
|
||||
back_url="/portal",
|
||||
back_url="/portal/apps",
|
||||
sections=[
|
||||
("Výstup katalogu", catalog_result.stdout),
|
||||
("Chyba katalogu", catalog_result.stderr),
|
||||
|
||||
Reference in New Issue
Block a user