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("/") @router.get("/")
def portal_home(user=Depends(require_user)): 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) @router.get("/apps", response_class=HTMLResponse)
@@ -373,21 +373,26 @@ def apps_page(
</div> </div>
</details> </details>
</td> </td>
<td class="actions-cell service-actions"> <td class="action-icons-cell">
<a class="btn" href="/portal/apps/{app_url_id}">Detail</a> <div class="action-icons">
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení</a> <a class="icon-action" href="/portal/apps/{app_url_id}" title="Detail služby" aria-label="Detail služby">
<details class="action-menu"> <i class="fa-solid fa-circle-info" aria-hidden="true"></i>
<summary aria-label="Další akce">...</summary> </a>
<div class="action-menu-panel"> <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}?');"> <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>
<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ář.');"> <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}"> <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> </form>
</div> </div>
</details>
</td> </td>
</tr> </tr>
""" """
+4 -4
View File
@@ -33,7 +33,7 @@ GOOGLE_USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"
@router.get("/login", response_class=HTMLResponse) @router.get("/login", response_class=HTMLResponse)
def login_form(request: Request): def login_form(request: Request):
if current_user(request): if current_user(request):
return RedirectResponse(url="/portal/operations", status_code=303) return RedirectResponse(url="/portal/apps", status_code=303)
return _render_login() return _render_login()
@@ -58,13 +58,13 @@ def login(request: Request, username: str = Form(...), password: str = Form(...)
metadata={"username": user.get("username")}, 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") @router.get("/auth/gitea/login")
def gitea_login(request: Request): def gitea_login(request: Request):
if current_user(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.") return _render_login("Gitea login is disabled. Use Google sign in.")
gitea_url = get_gitea_public_url() 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") @router.get("/auth/google/login")
def google_login(request: Request): def google_login(request: Request):
if current_user(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": if get_auth_mode() == "local":
return _render_login("Google login is not enabled in local auth mode.") 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); 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 { .running-row td {
background: var(--warning-bg); 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: if user:
username = html.escape(user.get("username", "")) username = html.escape(user.get("username", ""))
display_name = html.escape(user.get("display_name") or 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": if (user.get("role") or "").lower() == "admin":
admin_links = '<a href="/portal/admin/users">Users</a>' admin_menu = """
nav = f"""
<nav>
<a class="nav-link" href="/portal/operations">Přehled</a>
<a class="nav-link" href="/portal/apps">Služby</a>
<div class="nav-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"> <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/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/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/incidents">Incidenty</a>
<a href="/portal/workers">Workery</a> <a href="/portal/workers">Workery</a>
</div> <a href="/portal/admin/users">Users</a>
</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/audit">Audit</a> <a href="/portal/audit">Audit</a>
<a href="/portal/backups">Zálohy</a> <a href="/portal/backups">Z&aacute;lohy</a>
</div> </div>
</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> </nav>
""" """
user_panel = f""" user_panel = f"""
@@ -55,12 +56,14 @@ def page(title: str, body: str, user=None) -> str:
<html> <html>
<head> <head>
<title>{APP_NAME} - {html.escape(title)}</title> <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"> <link rel="stylesheet" href="{PORTAL_PREFIX}/static/styles.css">
<script src="{PORTAL_PREFIX}/static/portal.js"></script> <script src="{PORTAL_PREFIX}/static/portal.js"></script>
</head> </head>
<body> <body>
<header> <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"> <img src="{PORTAL_PREFIX}/static/csbot-logo.svg" alt="CSBOT">
<span>{APP_NAME}</span> <span>{APP_NAME}</span>
</a> </a>