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()
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user