update seznamu sluzeb
This commit is contained in:
+80
-58
@@ -45,6 +45,28 @@ def render_health_status(status: str | None) -> str:
|
||||
return f'<span class="{class_name}">{html.escape(labels.get(normalized, value or "neznámá"))}</span>'
|
||||
|
||||
|
||||
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'<span class="{class_name}" title="{html.escape(title)}" aria-label="{html.escape(title)}"></span>'
|
||||
|
||||
|
||||
@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'<option value="{value}" {selected}>{label}</option>'
|
||||
|
||||
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'<option value="{value}" {selected}>{label}</option>'
|
||||
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{app_id}</strong><br>
|
||||
<span class="muted">/apps/{app_id}</span>
|
||||
<tr class="service-row">
|
||||
<td class="service-name-cell">
|
||||
<div class="service-title">
|
||||
{health_dot}
|
||||
<div>
|
||||
<strong>{app_id}</strong><br>
|
||||
<span class="muted">/apps/{app_id}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="pill">{status}</span></td>
|
||||
<td>{health_status}<br><span class="muted">{health_checked_at}</span></td>
|
||||
<td><a href="{docs}">Swagger</a></td>
|
||||
<td><a href="{docs}" target="_blank" rel="noopener">Swagger</a></td>
|
||||
<td>
|
||||
<form method="post" action="/portal/update-resources">
|
||||
<input type="hidden" name="app_id" value="{app_id}">
|
||||
<div class="inline-form">
|
||||
<select name="memory">{memory_options}</select>
|
||||
<select name="cpus">{cpu_options}</select>
|
||||
<button type="submit">Použít</button>
|
||||
</div>
|
||||
<div class="resource-help">
|
||||
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.
|
||||
</div>
|
||||
</form>
|
||||
<div class="resource-summary">
|
||||
<span>Paměť: <strong>{memory_label}</strong></span>
|
||||
<span>CPU: <strong>{cpus_label}</strong></span>
|
||||
<button type="button" class="btn btn-compact btn-secondary" onclick="openResourceModal('{resource_modal_id}')">Upravit</button>
|
||||
</div>
|
||||
<dialog class="resource-modal" id="{resource_modal_id}">
|
||||
<form method="post" action="/portal/update-resources">
|
||||
<input type="hidden" name="app_id" value="{app_id}">
|
||||
<div class="modal-header">
|
||||
<h3>Prostředky služby</h3>
|
||||
<button type="button" class="icon-button" onclick="closeResourceModal('{resource_modal_id}')" aria-label="Zavřít">×</button>
|
||||
</div>
|
||||
<p class="muted">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>
|
||||
<label>Paměť</label>
|
||||
<input name="memory" value="{memory}" placeholder="např. 512m, 1g nebo prázdné pro výchozí">
|
||||
<label>CPU</label>
|
||||
<input name="cpus" value="{cpus}" placeholder="např. 0.50, 1.00 nebo prázdné pro výchozí">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeResourceModal('{resource_modal_id}')">Zrušit</button>
|
||||
<button type="submit">Použít</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</td>
|
||||
<td>
|
||||
<details>
|
||||
<details class="clone-details">
|
||||
<summary>Příkazy pro klonování</summary>
|
||||
<label class="muted">HTTP</label>
|
||||
<div class="cmd-row">
|
||||
@@ -169,22 +187,27 @@ def apps_page(
|
||||
</div>
|
||||
</details>
|
||||
</td>
|
||||
<td>
|
||||
<p><a class="btn" href="/portal/apps/{app_url_id}">Detail</a></p>
|
||||
<p><a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení</a></p>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit nové nasazení služby {app_id}?');">
|
||||
<button type="submit" class="btn-secondary">Nasadit znovu</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/delete-app" onsubmit="return confirm('Smazat {app_id}? Tím se odstraní kontejner, image, workspace, záznam v katalogu a Gitea repozitář.');">
|
||||
<input type="hidden" name="app_id" value="{app_id}">
|
||||
<button type="submit" class="danger">Smazat</button>
|
||||
</form>
|
||||
<td class="actions-cell service-actions">
|
||||
<a class="btn" href="/portal/apps/{app_url_id}">Detail</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení</a>
|
||||
<details class="action-menu">
|
||||
<summary aria-label="Další akce">...</summary>
|
||||
<div class="action-menu-panel">
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit nové nasazení služby {app_id}?');">
|
||||
<button type="submit" class="menu-action">Nasadit znovu</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/delete-app" onsubmit="return confirm('Smazat {app_id}? Tím se odstraní kontejner, image, workspace, záznam v katalogu a Gitea repozitář.');">
|
||||
<input type="hidden" name="app_id" value="{app_id}">
|
||||
<button type="submit" class="menu-action menu-action-danger">Smazat</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="7">Zatím nejsou nasazené žádné služby.</td></tr>'
|
||||
rows = '<tr><td colspan="6">Zatím nejsou nasazené žádné služby.</td></tr>'
|
||||
status_options = ['<option value="">Všechny stavy</option>']
|
||||
for value in status_values:
|
||||
selected = " selected" if selected_status == value else ""
|
||||
@@ -265,9 +288,8 @@ def apps_page(
|
||||
<tr>
|
||||
<th>Služba</th>
|
||||
<th>Status</th>
|
||||
<th>Zdraví</th>
|
||||
<th>Dokumentace</th>
|
||||
<th>Prostředky</th>
|
||||
<th>Prostředky <span class="info-dot" title="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.">i</span></th>
|
||||
<th>Git</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user