deployment observability, filters, stats, details
This commit is contained in:
+125
-4
@@ -1,7 +1,8 @@
|
||||
import html
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from ..auth import require_user
|
||||
from ..config import (
|
||||
@@ -13,9 +14,10 @@ from ..config import (
|
||||
NEW_APP_SCRIPT,
|
||||
read_env_value,
|
||||
)
|
||||
from ..db.apps import get_apps, update_app_resources
|
||||
from ..db.apps import get_app, get_app_deployments, get_apps, update_app_resources
|
||||
from ..db.audit import log_audit_event
|
||||
from ..shell import run_command
|
||||
from ..routes.deployments import render_status_pill
|
||||
from ..shell import run_command, run_command_background
|
||||
from ..templates.layout import page, render_result
|
||||
|
||||
router = APIRouter()
|
||||
@@ -33,6 +35,7 @@ def index(request: Request, user=Depends(require_user)):
|
||||
|
||||
for item in apps:
|
||||
app_id = html.escape(item.get("id", ""))
|
||||
app_url_id = quote(item.get("id", ""), safe="")
|
||||
status = html.escape(item.get("status", ""))
|
||||
docs = html.escape(item.get("docs", f"/apps/{app_id}/docs"))
|
||||
memory = item.get("memory", "")
|
||||
@@ -102,6 +105,11 @@ def index(request: Request, user=Depends(require_user)):
|
||||
</details>
|
||||
</td>
|
||||
<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>
|
||||
<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}">
|
||||
<button type="submit" class="danger">Smazat</button>
|
||||
@@ -153,6 +161,119 @@ def index(request: Request, user=Depends(require_user)):
|
||||
)
|
||||
|
||||
|
||||
@router.get("/apps/{app_id}", response_class=HTMLResponse)
|
||||
def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
app = get_app(app_id)
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
escaped_app_id = html.escape(app.get("id", ""))
|
||||
app_url_id = quote(app.get("id", ""), safe="")
|
||||
name = html.escape(app.get("name", "") or "")
|
||||
language = html.escape(app.get("language", "") or "")
|
||||
version = html.escape(app.get("version", "") or "")
|
||||
status = html.escape(app.get("status", "") or "")
|
||||
memory = html.escape(app.get("memory", "") or "")
|
||||
cpus = html.escape(app.get("cpus", "") or "")
|
||||
updated_at = html.escape(app.get("updated_at", "") or "")
|
||||
|
||||
rows = ""
|
||||
for deployment in get_app_deployments(app.get("id", ""), limit=10):
|
||||
deployment_id = html.escape(str(deployment.get("id", "")))
|
||||
started_at = html.escape(deployment.get("started_at", "") or "")
|
||||
triggered_by = html.escape(
|
||||
deployment.get("triggered_by_display_name")
|
||||
or deployment.get("triggered_by_username")
|
||||
or ""
|
||||
)
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td><a href="/portal/deployments/{deployment_id}">#{deployment_id}</a></td>
|
||||
<td>{render_status_pill(deployment.get("status"))}</td>
|
||||
<td>{started_at}</td>
|
||||
<td>{triggered_by}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="4">Zatím nejsou evidovaná žádná nasazení této aplikace.</td></tr>'
|
||||
|
||||
return page(
|
||||
f"Aplikace {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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Souhrn</h2>
|
||||
<table>
|
||||
<tr><th>ID</th><td>{escaped_app_id}</td></tr>
|
||||
<tr><th>Název</th><td>{name}</td></tr>
|
||||
<tr><th>Jazyk</th><td>{language}</td></tr>
|
||||
<tr><th>Verze</th><td>{version}</td></tr>
|
||||
<tr><th>Status</th><td><span class="pill">{status}</span></td></tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Poslední deploymenty</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Triggered by</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/redeploy")
|
||||
def redeploy_app(app_id: str, user=Depends(require_user)):
|
||||
app = get_app(app_id)
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
process = run_command_background(
|
||||
[DEPLOY_SCRIPT, app_id],
|
||||
extra_env={
|
||||
"APPFACTORY_TRIGGER_SOURCE": "portal",
|
||||
"APPFACTORY_TRIGGERED_BY_USER_ID": user.get("id"),
|
||||
"APPFACTORY_TRIGGERED_BY_USERNAME": user.get("username"),
|
||||
"APPFACTORY_TRIGGERED_BY_DISPLAY_NAME": user.get("display_name") or user.get("username"),
|
||||
},
|
||||
)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="app.redeploy",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={
|
||||
"app_id": app_id,
|
||||
"pid": process.pid,
|
||||
"trigger_source": "portal",
|
||||
},
|
||||
)
|
||||
|
||||
return RedirectResponse(url=f"/portal/apps/{quote(app_id, safe='')}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/new-app", response_class=HTMLResponse)
|
||||
def new_app_form(request: Request, user=Depends(require_user)):
|
||||
return page(
|
||||
|
||||
Reference in New Issue
Block a user