From 1ade865df47817408c182a26384a2e0bea4c779e Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Fri, 29 May 2026 12:07:13 +0200 Subject: [PATCH] update seznamu sluzeb --- app/routes/apps.py | 138 ++++++++++++---------- app/static/portal.js | 35 ++++++ app/static/styles.css | 258 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 58 deletions(-) diff --git a/app/routes/apps.py b/app/routes/apps.py index ff3ae65..7b183d6 100644 --- a/app/routes/apps.py +++ b/app/routes/apps.py @@ -45,6 +45,28 @@ def render_health_status(status: str | None) -> str: return f'{html.escape(labels.get(normalized, value or "neznámá"))}' +def render_health_dot(status: str | None, checked_at: str | None) -> str: + normalized = (status or "").lower() + labels = { + "healthy": "zdravá", + "unhealthy": "nezdravá", + "unreachable": "nedostupná", + } + class_name = "health-dot health-dot-muted" + if normalized == "healthy": + class_name = "health-dot health-dot-success" + elif normalized == "unhealthy": + class_name = "health-dot health-dot-warning" + elif normalized == "unreachable": + class_name = "health-dot health-dot-danger" + + label = labels.get(normalized, "zdraví neznámé") + title = label + if checked_at: + title = f"{label}, poslední kontrola: {checked_at}" + return f'' + + @router.get("/") def portal_home(user=Depends(require_user)): return RedirectResponse(url="/portal/operations", status_code=303) @@ -94,67 +116,63 @@ def apps_page( rows = "" - for item in apps: + for index, item in enumerate(apps, start=1): app_id = html.escape(item.get("id", "")) app_url_id = quote(item.get("id", ""), safe="") + resource_modal_id = f"resources-{page_number}-{index}" status = html.escape(item.get("status", "")) health = latest_health.get(item.get("id", "")) or {} - health_status = render_health_status(health.get("status")) health_checked_at = html.escape(health.get("checked_at", "") or "") + health_dot = render_health_dot(health.get("status"), health.get("checked_at")) docs = html.escape(item.get("docs", f"/apps/{app_id}/docs")) - memory = item.get("memory", "") - cpus = item.get("cpus", "") + memory = html.escape(item.get("memory", "") or "") + cpus = html.escape(item.get("cpus", "") or "") + memory_label = memory or "výchozí" + cpus_label = cpus or "výchozí" http_clone = html.escape(f"git clone {gitea_url}/{gitea_org}/{app_id}.git") ssh_clone = html.escape(f"git clone ssh://git@{host}:2222/{gitea_org}/{app_id}.git") - memory_options = "" - for value, label in [ - ("", "Výchozí"), - ("256m", "256 MB - malá služba"), - ("512m", "512 MB - běžná služba"), - ("1g", "1 GB - větší služba"), - ("2g", "2 GB - náročná služba"), - ]: - selected = "selected" if memory == value else "" - memory_options += f'' - - cpu_options = "" - for value, label in [ - ("", "Výchozí"), - ("0.25", "0,25 CPU - velmi malá služba"), - ("0.50", "0,50 CPU - běžná služba"), - ("1.00", "1 CPU - celé jádro"), - ("2.00", "2 CPU - náročná služba"), - ]: - selected = "selected" if cpus == value else "" - cpu_options += f'' - rows += f""" - - - {app_id}
- /apps/{app_id} + + +
+ {health_dot} +
+ {app_id}
+ /apps/{app_id} +
+
{status} - {health_status}
{health_checked_at} - Swagger + Swagger -
- -
- - - -
-
- Paměť je limit RAM. CPU určuje maximální podíl výpočetního výkonu. - Příklad: 0,50 = polovina jádra, 1,00 = celé jádro. -
-
+
+ Paměť: {memory_label} + CPU: {cpus_label} + +
+ +
+ + +

Paměť je limit RAM. CPU určuje maximální podíl výpočetního výkonu. Příklad: 0,50 = polovina jádra, 1,00 = celé jádro.

+ + + + + +
+
-
+
Příkazy pro klonování
@@ -169,22 +187,27 @@ def apps_page(
- -

Detail

-

Nasazení

-
- -
-
- - -
+ + Detail + Nasazení +
+ ... +
+
+ +
+
+ + +
+
+
""" if not rows: - rows = 'Zatím nejsou nasazené žádné služby.' + rows = 'Zatím nejsou nasazené žádné služby.' status_options = [''] for value in status_values: selected = " selected" if selected_status == value else "" @@ -265,9 +288,8 @@ def apps_page( Služba Status - Zdraví Dokumentace - Prostředky + Prostředky i Git Akce diff --git a/app/static/portal.js b/app/static/portal.js index 1c6f749..247d88c 100644 --- a/app/static/portal.js +++ b/app/static/portal.js @@ -82,3 +82,38 @@ function copyText(button) { .catch(() => showCopied(button, false)); return false; } + +function openResourceModal(id) { + const modal = document.getElementById(id); + if (!modal) { + return false; + } + + if (typeof modal.showModal === "function") { + modal.showModal(); + } else { + modal.setAttribute("open", ""); + } + return false; +} + +function closeResourceModal(id) { + const modal = document.getElementById(id); + if (!modal) { + return false; + } + + if (typeof modal.close === "function") { + modal.close(); + } else { + modal.removeAttribute("open"); + } + return false; +} + +document.addEventListener("click", (event) => { + const modal = event.target; + if (typeof HTMLDialogElement !== "undefined" && modal instanceof HTMLDialogElement && modal.classList.contains("resource-modal")) { + modal.close(); + } +}); diff --git a/app/static/styles.css b/app/static/styles.css index f250b0a..28cb2b1 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -397,6 +397,51 @@ button:hover, color: var(--muted); } +.health-dot { + display: inline-block; + width: 11px; + height: 11px; + border-radius: 999px; + background: var(--muted); + box-shadow: 0 0 0 4px rgba(98, 121, 135, 0.14); + flex: 0 0 auto; +} + +.health-dot-success { + background: var(--success); + box-shadow: 0 0 0 4px var(--success-bg); +} + +.health-dot-warning { + background: var(--warning); + box-shadow: 0 0 0 4px var(--warning-bg); +} + +.health-dot-danger { + background: var(--danger); + box-shadow: 0 0 0 4px var(--danger-bg); +} + +.health-dot-muted { + background: var(--muted); +} + +.info-dot { + display: inline-flex; + width: 18px; + height: 18px; + margin-left: 4px; + align-items: center; + justify-content: center; + border: 1px solid var(--muted); + border-radius: 999px; + color: var(--muted); + font-size: 12px; + font-weight: 800; + line-height: 1; + text-transform: none; +} + input, select { padding: 8px; @@ -447,6 +492,192 @@ input[readonly] { margin: 0 0 8px; } +.service-row td { + vertical-align: middle; +} + +.service-name-cell { + min-width: 260px; +} + +.service-title { + display: flex; + align-items: center; + gap: 12px; +} + +.resource-summary { + display: grid; + grid-template-columns: 1fr; + gap: 5px; + min-width: 170px; +} + +.resource-summary span { + color: var(--muted); + font-size: 13px; +} + +.resource-summary strong { + color: var(--text); +} + +.btn-compact { + width: fit-content; + min-height: 30px; + padding: 6px 10px; + font-size: 13px; +} + +.resource-modal { + width: min(520px, calc(100vw - 32px)); + border: 1px solid var(--border); + border-radius: 8px; + padding: 20px; + color: var(--text); + box-shadow: 0 18px 48px rgba(10, 31, 42, 0.26); +} + +.resource-modal::backdrop { + background: rgba(10, 31, 42, 0.34); +} + +.resource-modal form { + display: grid; + gap: 10px; + margin: 0; +} + +.resource-modal h3 { + margin: 0; + color: var(--secondary); +} + +.resource-modal label { + font-weight: 800; + color: var(--secondary); +} + +.resource-modal input { + width: 100%; +} + +.modal-header, +.modal-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.modal-actions { + justify-content: flex-end; + margin-top: 8px; +} + +.icon-button { + width: 34px; + min-height: 34px; + padding: 0; + border: 1px solid var(--border); + background: white; + color: var(--secondary); + font-size: 22px; + line-height: 1; +} + +.icon-button:hover { + background: #eef6f9; + color: var(--secondary-dark); +} + +.clone-details { + min-width: 170px; +} + +.clone-details summary { + color: var(--text); +} + +.service-actions { + display: flex; + align-items: center; + gap: 8px; + width: auto; + min-width: 250px; +} + +.service-actions .btn { + margin: 0; +} + +.action-menu { + position: relative; +} + +.action-menu summary { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + min-height: 34px; + border-radius: 8px; + color: var(--secondary); + font-weight: 900; + list-style: none; + cursor: pointer; +} + +.action-menu summary::-webkit-details-marker { + display: none; +} + +.action-menu summary:hover, +.action-menu[open] summary { + background: #eef6f9; + text-decoration: none; +} + +.action-menu-panel { + position: absolute; + top: calc(100% + 6px); + right: 0; + z-index: 12; + width: 210px; + padding: 6px; + border: 1px solid var(--border); + border-radius: 8px; + background: white; + box-shadow: 0 16px 34px rgba(10, 31, 42, 0.18); +} + +.action-menu-panel form { + margin: 0; +} + +.menu-action { + width: 100%; + padding: 10px 12px; + background: white; + color: var(--text); + text-align: left; + font-weight: 700; +} + +.menu-action:hover { + background: #eef6f9; + color: var(--secondary-dark); +} + +.menu-action-danger { + color: var(--danger); +} + +.menu-action-danger:hover { + background: var(--danger-bg); + color: var(--danger-dark); +} + .running-row td { background: var(--warning-bg); } @@ -640,6 +871,33 @@ pre { width: 100%; } + .service-title { + align-items: flex-start; + } + + .service-actions { + align-items: stretch; + flex-direction: column; + min-width: 180px; + } + + .service-actions .btn, + .service-actions .action-menu, + .action-menu summary { + width: 100%; + } + + .action-menu-panel { + position: static; + width: 100%; + margin-top: 6px; + } + + .modal-actions { + align-items: stretch; + flex-direction: column-reverse; + } + .pagination { align-items: flex-start; flex-direction: column;