strankovani u deploys a styly pro pocitadla

This commit is contained in:
JiriUhlir
2026-05-28 13:26:58 +02:00
parent e6ffeb8fef
commit 8624352253
3 changed files with 152 additions and 10 deletions
+37 -5
View File
@@ -81,14 +81,11 @@ def update_app_resources(app_id: str, memory: str, cpus: str):
con.close()
def get_deployments(
limit: int = 100,
def build_deployment_filters(
status: str | None = None,
app_id: str | None = None,
source: str | None = None,
):
con = get_connection()
filters = []
params = []
@@ -116,6 +113,19 @@ def get_deployments(
params.append(source)
where = f"WHERE {' AND '.join(filters)}" if filters else ""
return where, params
def get_deployments(
limit: int = 20,
offset: int = 0,
status: str | None = None,
app_id: str | None = None,
source: str | None = None,
):
con = get_connection()
where, params = build_deployment_filters(status=status, app_id=app_id, source=source)
rows = con.execute(
f"""
SELECT
@@ -138,14 +148,36 @@ def get_deployments(
END,
id DESC
LIMIT ?
OFFSET ?
""",
(*params, *RUNNING_STATUSES, limit),
(*params, *RUNNING_STATUSES, limit, offset),
).fetchall()
con.close()
return [dict(row) for row in rows]
def count_deployments(
status: str | None = None,
app_id: str | None = None,
source: str | None = None,
):
con = get_connection()
where, params = build_deployment_filters(status=status, app_id=app_id, source=source)
row = con.execute(
f"""
SELECT COUNT(*) AS count
FROM deployments
{where}
""",
params,
).fetchone()
con.close()
return row["count"] if row else 0
def get_deployment_filter_options():
con = get_connection()
+52 -5
View File
@@ -1,12 +1,14 @@
import html
import math
from datetime import datetime
from urllib.parse import quote
from urllib.parse import quote, urlencode
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, PlainTextResponse
from app.auth import require_user
from app.db.apps import (
count_deployments,
get_deployment,
get_deployment_filter_options,
get_deployment_stats,
@@ -19,6 +21,7 @@ router = APIRouter()
SUCCESS_STATUSES = {"ok", "success", "succeeded", "done", "deployed", "completed"}
RUNNING_STATUSES = {"running", "pending", "queued", "in_progress", "starting"}
FAILED_STATUSES = {"failed", "failure", "error", "cancelled", "canceled"}
DEFAULT_PAGE_SIZE = 20
def normalize_status(status: str | None) -> str:
@@ -115,18 +118,43 @@ def render_options(values: list[str], selected: str | None, empty_label: str) ->
return "".join(options)
def pagination_url(page_number: int, status: str, app_id: str, source: str) -> str:
params = {"page": page_number}
if status:
params["status"] = status
if app_id:
params["app_id"] = app_id
if source:
params["source"] = source
return f"/portal/deployments?{urlencode(params)}"
@router.get("/deployments", response_class=HTMLResponse)
async def deployments_page(
request: Request,
status: str = Query(""),
app_id: str = Query(""),
source: str = Query(""),
page_number: int = Query(1, alias="page", ge=1),
user=Depends(require_user),
):
selected_status = status.strip()
selected_app = app_id.strip()
selected_source = source.strip()
offset = (page_number - 1) * DEFAULT_PAGE_SIZE
total_deployments = count_deployments(
status=selected_status or None,
app_id=selected_app or None,
source=selected_source or None,
)
total_pages = max(1, math.ceil(total_deployments / DEFAULT_PAGE_SIZE))
if page_number > total_pages:
page_number = total_pages
offset = (page_number - 1) * DEFAULT_PAGE_SIZE
deployments = get_deployments(
limit=DEFAULT_PAGE_SIZE,
offset=offset,
status=selected_status or None,
app_id=selected_app or None,
source=selected_source or None,
@@ -179,6 +207,22 @@ async def deployments_page(
status_options = render_options(status_values, selected_status, "Všechny statusy")
app_options = render_options(options["apps"], selected_app, "Všechny aplikace")
source_options = render_options(options["sources"], selected_source, "Všechny zdroje")
first_item = offset + 1 if total_deployments else 0
last_item = min(offset + len(deployments), total_deployments)
previous_disabled = " disabled" if page_number <= 1 else ""
next_disabled = " disabled" if page_number >= total_pages else ""
previous_href = pagination_url(max(1, page_number - 1), selected_status, selected_app, selected_source)
next_href = pagination_url(min(total_pages, page_number + 1), selected_status, selected_app, selected_source)
pagination = f"""
<div class="pagination">
<span>Zobrazeno {first_item}-{last_item} z {total_deployments}</span>
<div class="pagination-actions">
<a class="btn btn-secondary{previous_disabled}" href="{previous_href}" aria-disabled="{str(page_number <= 1).lower()}">Předchozí</a>
<span class="page-indicator">Strana {page_number} / {total_pages}</span>
<a class="btn btn-secondary{next_disabled}" href="{next_href}" aria-disabled="{str(page_number >= total_pages).lower()}">Další</a>
</div>
</div>
"""
return page(
"Nasazení",
@@ -191,10 +235,10 @@ async def deployments_page(
</div>
<div class="stats-grid">
<div class="stat-card"><span>Total deploys</span><strong>{stats.get("total") or 0}</strong></div>
<div class="stat-card"><span>Failed deploys</span><strong>{stats.get("failed") or 0}</strong></div>
<div class="stat-card"><span>Running deploys</span><strong>{stats.get("running") or 0}</strong></div>
<div class="stat-card"><span>Success rate</span><strong>{stats.get("success_rate") or 0}%</strong></div>
<div class="stat-card stat-total"><span>Total deploys</span><strong>{stats.get("total") or 0}</strong></div>
<div class="stat-card stat-danger"><span>Failed deploys</span><strong>{stats.get("failed") or 0}</strong></div>
<div class="stat-card stat-warning"><span>Running deploys</span><strong>{stats.get("running") or 0}</strong></div>
<div class="stat-card stat-success"><span>Success rate</span><strong>{stats.get("success_rate") or 0}%</strong></div>
</div>
<div class="card">
@@ -203,6 +247,7 @@ async def deployments_page(
<select name="status">{status_options}</select>
<select name="app_id">{app_options}</select>
<select name="source">{source_options}</select>
<input type="hidden" name="page" value="1">
<button type="submit">Filtrovat</button>
<a class="btn btn-secondary" href="/portal/deployments">Reset</a>
</form>
@@ -210,6 +255,7 @@ async def deployments_page(
<div class="card">
<h2>Nasazení</h2>
{pagination}
<table>
<tr>
<th>ID</th>
@@ -224,6 +270,7 @@ async def deployments_page(
</tr>
{rows}
</table>
{pagination}
</div>
""",
user=user,
+63
View File
@@ -148,6 +148,7 @@ main {
.stat-card {
background: var(--card);
border: 1px solid var(--border);
border-left: 5px solid var(--secondary);
border-radius: 8px;
padding: 18px;
box-shadow: 0 8px 24px rgba(30, 81, 107, 0.07);
@@ -168,6 +169,37 @@ main {
color: var(--secondary);
}
.stat-success {
border-left-color: var(--success);
background: linear-gradient(90deg, var(--success-bg), var(--card) 46%);
}
.stat-success strong {
color: var(--success);
}
.stat-warning {
border-left-color: var(--warning);
background: linear-gradient(90deg, var(--warning-bg), var(--card) 46%);
}
.stat-warning strong {
color: var(--warning);
}
.stat-danger {
border-left-color: var(--danger);
background: linear-gradient(90deg, var(--danger-bg), var(--card) 46%);
}
.stat-danger strong {
color: var(--danger);
}
.stat-total {
border-left-color: var(--secondary);
}
table {
border-collapse: collapse;
width: 100%;
@@ -232,6 +264,14 @@ button:hover,
background: var(--secondary-dark);
}
.btn.disabled,
.btn.disabled:hover {
background: #b8c8d0;
color: #f8fbfc;
cursor: not-allowed;
pointer-events: none;
}
.danger {
background: var(--danger);
}
@@ -392,3 +432,26 @@ pre {
align-items: center;
flex-wrap: wrap;
}
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
margin: 12px 0;
}
.pagination-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.page-indicator {
color: var(--secondary);
white-space: nowrap;
}