UI a menu a FA

This commit is contained in:
JiriUhlir
2026-06-15 09:57:24 +02:00
parent 40069c8969
commit 3bb762d08a
8 changed files with 117 additions and 38 deletions
+15 -10
View File
@@ -253,7 +253,7 @@ def render_health_dot(status: str | None, checked_at: str | None) -> str:
@router.get("/")
def portal_home(user=Depends(require_user)):
return RedirectResponse(url="/portal/operations", status_code=303)
return RedirectResponse(url="/portal/apps", status_code=303)
@router.get("/apps", response_class=HTMLResponse)
@@ -373,21 +373,26 @@ def apps_page(
</div>
</details>
</td>
<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">
<td class="action-icons-cell">
<div class="action-icons">
<a class="icon-action" href="/portal/apps/{app_url_id}" title="Detail služby" aria-label="Detail služby">
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
</a>
<a class="icon-action" href="/portal/deployments?app_id={app_url_id}" title="Nasazení" aria-label="Nasazení">
<i class="fa-solid fa-rocket" aria-hidden="true"></i>
</a>
<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>
<button type="submit" class="icon-action" title="Nasadit znovu" aria-label="Nasadit znovu">
<i class="fa-solid fa-arrows-rotate" aria-hidden="true"></i>
</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>
<button type="submit" class="icon-action icon-action-danger" title="Smazat" aria-label="Smazat">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
</button>
</form>
</div>
</details>
</td>
</tr>
"""
+4 -4
View File
@@ -33,7 +33,7 @@ GOOGLE_USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"
@router.get("/login", response_class=HTMLResponse)
def login_form(request: Request):
if current_user(request):
return RedirectResponse(url="/portal/operations", status_code=303)
return RedirectResponse(url="/portal/apps", status_code=303)
return _render_login()
@@ -58,13 +58,13 @@ def login(request: Request, username: str = Form(...), password: str = Form(...)
metadata={"username": user.get("username")},
)
return RedirectResponse(url="/portal/operations", status_code=303)
return RedirectResponse(url="/portal/apps", status_code=303)
@router.get("/auth/gitea/login")
def gitea_login(request: Request):
if current_user(request):
return RedirectResponse(url="/portal/operations", status_code=303)
return RedirectResponse(url="/portal/apps", status_code=303)
return _render_login("Gitea login is disabled. Use Google sign in.")
gitea_url = get_gitea_public_url()
@@ -148,7 +148,7 @@ def gitea_callback(request: Request, code: str = "", state: str = "", error: str
@router.get("/auth/google/login")
def google_login(request: Request):
if current_user(request):
return RedirectResponse(url="/portal/operations", status_code=303)
return RedirectResponse(url="/portal/apps", status_code=303)
if get_auth_mode() == "local":
return _render_login("Google login is not enabled in local auth mode.")
+56
View File
@@ -816,6 +816,62 @@ input[readonly] {
color: var(--danger-dark);
}
.action-icons-cell {
width: 1%;
white-space: nowrap;
vertical-align: middle;
}
.action-icons {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.action-icons form {
margin: 0;
display: inline-flex;
}
.icon-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
min-height: 38px;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: white;
color: var(--secondary);
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.icon-action:hover,
.icon-action:focus-visible {
background: #eef6f9;
color: var(--secondary-dark);
border-color: var(--secondary);
text-decoration: none;
outline: none;
}
.icon-action-danger {
color: var(--danger);
}
.icon-action-danger:hover,
.icon-action-danger:focus-visible {
background: var(--danger-bg);
color: var(--danger-dark);
border-color: var(--danger);
}
.running-row td {
background: var(--warning-bg);
}
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
Binary file not shown.
Binary file not shown.
+21 -18
View File
@@ -12,33 +12,34 @@ def page(title: str, body: str, user=None) -> str:
if user:
username = html.escape(user.get("username", ""))
display_name = html.escape(user.get("display_name") or user.get("username", ""))
admin_links = ""
admin_menu = ""
if (user.get("role") or "").lower() == "admin":
admin_links = '<a href="/portal/admin/users">Users</a>'
nav = f"""
<nav>
<a class="nav-link" href="/portal/operations">Přehled</a>
<a class="nav-link" href="/portal/apps">Služby</a>
admin_menu = """
<div class="nav-menu">
<button type="button" class="nav-menu-button">Provoz</button>
<button type="button" class="nav-menu-button">Admin</button>
<div class="nav-menu-panel">
<a href="/portal/jobs">Úlohy</a>
<a href="/portal/operations">P&rcaron;ehled</a>
<a href="/portal/apps">Slu&zcaron;by</a>
<a href="/portal/jobs">&Uacute;lohy</a>
<a href="/portal/scheduled-scripts">Pl&aacute;novan&eacute; skripty</a>
<a href="/portal/migration-readiness">Migration Readiness</a>
<a href="/portal/alerting/rules">Alerting</a>
<a href="/portal/deployments">Nasazení</a>
<a href="/portal/migration-readiness">Migration Readiness</a>
<a href="/portal/deployments">Nasazen&iacute;</a>
<a href="/portal/incidents">Incidenty</a>
<a href="/portal/workers">Workery</a>
</div>
</div>
<div class="nav-menu">
<button type="button" class="nav-menu-button">Správa</button>
<div class="nav-menu-panel">
{admin_links}
<a href="/portal/admin/users">Users</a>
<a href="/portal/audit">Audit</a>
<a href="/portal/backups">Zálohy</a>
<a href="/portal/backups">Z&aacute;lohy</a>
</div>
</div>
"""
nav = f"""
<nav>
<a class="nav-link" href="/portal/apps">Slu&zcaron;by</a>
<a class="nav-link" href="/portal/jobs">&Uacute;lohy</a>
<a class="nav-link" href="/portal/scheduled-scripts">Pl&aacute;novan&eacute; skripty</a>
<a class="nav-link" href="/portal/alerting/rules">Alerting</a>
{admin_menu}
</nav>
"""
user_panel = f"""
@@ -55,12 +56,14 @@ def page(title: str, body: str, user=None) -> str:
<html>
<head>
<title>{APP_NAME} - {html.escape(title)}</title>
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/vendor/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/vendor/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/styles.css">
<script src="{PORTAL_PREFIX}/static/portal.js"></script>
</head>
<body>
<header>
<a class="brand" href="/portal/operations" aria-label="{APP_NAME}">
<a class="brand" href="/portal/apps" aria-label="{APP_NAME}">
<img src="{PORTAL_PREFIX}/static/csbot-logo.svg" alt="CSBOT">
<span>{APP_NAME}</span>
</a>