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() con.close()
def get_deployments( def build_deployment_filters(
limit: int = 100,
status: str | None = None, status: str | None = None,
app_id: str | None = None, app_id: str | None = None,
source: str | None = None, source: str | None = None,
): ):
con = get_connection()
filters = [] filters = []
params = [] params = []
@@ -116,6 +113,19 @@ def get_deployments(
params.append(source) params.append(source)
where = f"WHERE {' AND '.join(filters)}" if filters else "" 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( rows = con.execute(
f""" f"""
SELECT SELECT
@@ -138,14 +148,36 @@ def get_deployments(
END, END,
id DESC id DESC
LIMIT ? LIMIT ?
OFFSET ?
""", """,
(*params, *RUNNING_STATUSES, limit), (*params, *RUNNING_STATUSES, limit, offset),
).fetchall() ).fetchall()
con.close() con.close()
return [dict(row) for row in rows] 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(): def get_deployment_filter_options():
con = get_connection() con = get_connection()
+52 -5
View File
@@ -1,12 +1,14 @@
import html import html
import math
from datetime import datetime 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 import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, PlainTextResponse from fastapi.responses import HTMLResponse, PlainTextResponse
from app.auth import require_user from app.auth import require_user
from app.db.apps import ( from app.db.apps import (
count_deployments,
get_deployment, get_deployment,
get_deployment_filter_options, get_deployment_filter_options,
get_deployment_stats, get_deployment_stats,
@@ -19,6 +21,7 @@ router = APIRouter()
SUCCESS_STATUSES = {"ok", "success", "succeeded", "done", "deployed", "completed"} SUCCESS_STATUSES = {"ok", "success", "succeeded", "done", "deployed", "completed"}
RUNNING_STATUSES = {"running", "pending", "queued", "in_progress", "starting"} RUNNING_STATUSES = {"running", "pending", "queued", "in_progress", "starting"}
FAILED_STATUSES = {"failed", "failure", "error", "cancelled", "canceled"} FAILED_STATUSES = {"failed", "failure", "error", "cancelled", "canceled"}
DEFAULT_PAGE_SIZE = 20
def normalize_status(status: str | None) -> str: 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) 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) @router.get("/deployments", response_class=HTMLResponse)
async def deployments_page( async def deployments_page(
request: Request, request: Request,
status: str = Query(""), status: str = Query(""),
app_id: str = Query(""), app_id: str = Query(""),
source: str = Query(""), source: str = Query(""),
page_number: int = Query(1, alias="page", ge=1),
user=Depends(require_user), user=Depends(require_user),
): ):
selected_status = status.strip() selected_status = status.strip()
selected_app = app_id.strip() selected_app = app_id.strip()
selected_source = source.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( deployments = get_deployments(
limit=DEFAULT_PAGE_SIZE,
offset=offset,
status=selected_status or None, status=selected_status or None,
app_id=selected_app or None, app_id=selected_app or None,
source=selected_source 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") status_options = render_options(status_values, selected_status, "Všechny statusy")
app_options = render_options(options["apps"], selected_app, "Všechny aplikace") app_options = render_options(options["apps"], selected_app, "Všechny aplikace")
source_options = render_options(options["sources"], selected_source, "Všechny zdroje") 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( return page(
"Nasazení", "Nasazení",
@@ -191,10 +235,10 @@ async def deployments_page(
</div> </div>
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"><span>Total deploys</span><strong>{stats.get("total") 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"><span>Failed deploys</span><strong>{stats.get("failed") 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"><span>Running deploys</span><strong>{stats.get("running") 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"><span>Success rate</span><strong>{stats.get("success_rate") 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>
<div class="card"> <div class="card">
@@ -203,6 +247,7 @@ async def deployments_page(
<select name="status">{status_options}</select> <select name="status">{status_options}</select>
<select name="app_id">{app_options}</select> <select name="app_id">{app_options}</select>
<select name="source">{source_options}</select> <select name="source">{source_options}</select>
<input type="hidden" name="page" value="1">
<button type="submit">Filtrovat</button> <button type="submit">Filtrovat</button>
<a class="btn btn-secondary" href="/portal/deployments">Reset</a> <a class="btn btn-secondary" href="/portal/deployments">Reset</a>
</form> </form>
@@ -210,6 +255,7 @@ async def deployments_page(
<div class="card"> <div class="card">
<h2>Nasazení</h2> <h2>Nasazení</h2>
{pagination}
<table> <table>
<tr> <tr>
<th>ID</th> <th>ID</th>
@@ -224,6 +270,7 @@ async def deployments_page(
</tr> </tr>
{rows} {rows}
</table> </table>
{pagination}
</div> </div>
""", """,
user=user, user=user,
+63
View File
@@ -148,6 +148,7 @@ main {
.stat-card { .stat-card {
background: var(--card); background: var(--card);
border: 1px solid var(--border); border: 1px solid var(--border);
border-left: 5px solid var(--secondary);
border-radius: 8px; border-radius: 8px;
padding: 18px; padding: 18px;
box-shadow: 0 8px 24px rgba(30, 81, 107, 0.07); box-shadow: 0 8px 24px rgba(30, 81, 107, 0.07);
@@ -168,6 +169,37 @@ main {
color: var(--secondary); 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 { table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
@@ -232,6 +264,14 @@ button:hover,
background: var(--secondary-dark); background: var(--secondary-dark);
} }
.btn.disabled,
.btn.disabled:hover {
background: #b8c8d0;
color: #f8fbfc;
cursor: not-allowed;
pointer-events: none;
}
.danger { .danger {
background: var(--danger); background: var(--danger);
} }
@@ -392,3 +432,26 @@ pre {
align-items: center; align-items: center;
flex-wrap: wrap; 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;
}