From 86243522532874270d5e58cd4a68cf0f4d250c2f Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Thu, 28 May 2026 13:26:58 +0200 Subject: [PATCH] strankovani u deploys a styly pro pocitadla --- app/db/apps.py | 42 ++++++++++++++++++++++---- app/routes/deployments.py | 57 +++++++++++++++++++++++++++++++---- app/static/styles.css | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 10 deletions(-) diff --git a/app/db/apps.py b/app/db/apps.py index 298c309..df3110f 100644 --- a/app/db/apps.py +++ b/app/db/apps.py @@ -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() diff --git a/app/routes/deployments.py b/app/routes/deployments.py index 1ea3e15..76f9075 100644 --- a/app/routes/deployments.py +++ b/app/routes/deployments.py @@ -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""" + + """ return page( "Nasazení", @@ -191,10 +235,10 @@ async def deployments_page(
-
Total deploys{stats.get("total") or 0}
-
Failed deploys{stats.get("failed") or 0}
-
Running deploys{stats.get("running") or 0}
-
Success rate{stats.get("success_rate") or 0}%
+
Total deploys{stats.get("total") or 0}
+
Failed deploys{stats.get("failed") or 0}
+
Running deploys{stats.get("running") or 0}
+
Success rate{stats.get("success_rate") or 0}%
@@ -203,6 +247,7 @@ async def deployments_page( + Reset @@ -210,6 +255,7 @@ async def deployments_page(

Nasazení

+ {pagination} @@ -224,6 +270,7 @@ async def deployments_page( {rows}
ID
+ {pagination}
""", user=user, diff --git a/app/static/styles.css b/app/static/styles.css index 36bb1da..062884f 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -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; +}