strankovani u deploys a styly pro pocitadla
This commit is contained in:
+37
-5
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user