feat(admin): left sidebar layout, dark theme, role column

This commit is contained in:
Claus Lohmar 2026-06-14 18:54:47 +00:00
parent 863b87ffb7
commit da575701e4
7 changed files with 318 additions and 407 deletions

View file

@ -1,55 +1,20 @@
package templates
templ Dashboard(userCount int) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Admin Dashboard - Next Workspace</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/htmx.org@2.0.4/dist/ext/response-targets.js"></script>
<style>{ adminStyles() }</style>
</head>
<body>
<div class="app-container">
<nav class="sidebar">
<div class="sidebar-header">
<h1>NextWks</h1>
<span class="version">Admin</span>
@Layout("dashboard") {
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{ userCount }</div>
<div class="stat-label">Users</div>
</div>
<ul class="sidebar-nav">
<li><a href="/admin" class="nav-link">Dashboard</a></li>
<li><a href="/admin/users" class="nav-link">Users</a></li>
</ul>
<div class="sidebar-footer">
<span class="status-indicator" id="health-status">Connected</span>
<div class="stat-card">
<div class="stat-value">—</div>
<div class="stat-label">Modules</div>
</div>
</nav>
<main class="main-content">
<h2 style="margin-bottom:1.5rem;">Admin Dashboard</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;">
<div class="card" style="text-align:center;">
<div style="font-size:2rem;font-weight:700;color:var(--primary);">{ userCount }</div>
<div style="color:var(--text-muted);margin-top:0.25rem;">Total Users</div>
</div>
<div class="card" style="text-align:center;">
<div style="font-size:2rem;font-weight:700;color:var(--success);">Online</div>
<div style="color:var(--text-muted);margin-top:0.25rem;">Authelia API</div>
</div>
<div class="card" style="text-align:center;">
<div style="font-size:2rem;font-weight:700;color:var(--warning);">/opt/</div>
<div style="color:var(--text-muted);margin-top:0.25rem;">Runtime Path</div>
<div class="stat-card">
<div class="stat-value">OK</div>
<div class="stat-label">Status</div>
</div>
</div>
<div class="card mt-2">
<h2>Quick Actions</h2>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<a href="/admin/users" class="btn btn-primary">Manage Users</a>
</div>
</div>
</main>
</div>
</body>
</html>
}
}

View file

@ -29,20 +29,38 @@ func Dashboard(userCount int) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Admin Dashboard - Next Workspace</title><script src=\"https://unpkg.com/htmx.org@2.0.4\"></script><script src=\"https://unpkg.com/htmx.org@2.0.4/dist/ext/response-targets.js\"></script><style>{ adminStyles() }</style></head><body><div class=\"app-container\"><nav class=\"sidebar\"><div class=\"sidebar-header\"><h1>NextWks</h1><span class=\"version\">Admin</span></div><ul class=\"sidebar-nav\"><li><a href=\"/admin\" class=\"nav-link\">Dashboard</a></li><li><a href=\"/admin/users\" class=\"nav-link\">Users</a></li></ul><div class=\"sidebar-footer\"><span class=\"status-indicator\" id=\"health-status\">Connected</span></div></nav><main class=\"main-content\"><h2 style=\"margin-bottom:1.5rem;\">Admin Dashboard</h2><div style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;\"><div class=\"card\" style=\"text-align:center;\"><div style=\"font-size:2rem;font-weight:700;color:var(--primary);\">")
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"stats-grid\"><div class=\"stat-card\"><div class=\"stat-value\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(userCount)
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(userCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/dashboard.templ`, Line: 33, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/dashboard.templ`, Line: 7, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div style=\"color:var(--text-muted);margin-top:0.25rem;\">Total Users</div></div><div class=\"card\" style=\"text-align:center;\"><div style=\"font-size:2rem;font-weight:700;color:var(--success);\">Online</div><div style=\"color:var(--text-muted);margin-top:0.25rem;\">Authelia API</div></div><div class=\"card\" style=\"text-align:center;\"><div style=\"font-size:2rem;font-weight:700;color:var(--warning);\">/opt/</div><div style=\"color:var(--text-muted);margin-top:0.25rem;\">Runtime Path</div></div></div><div class=\"card mt-2\"><h2>Quick Actions</h2><div style=\"display:flex;gap:0.5rem;flex-wrap:wrap;\"><a href=\"/admin/users\" class=\"btn btn-primary\">Manage Users</a></div></div></main></div></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"stat-label\">Users</div></div><div class=\"stat-card\"><div class=\"stat-value\">—</div><div class=\"stat-label\">Modules</div></div><div class=\"stat-card\"><div class=\"stat-value\">OK</div><div class=\"stat-label\">Status</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("dashboard").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -1,36 +1,38 @@
package templates
templ BaseLayout(title string) {
templ Layout(page string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title } - Next Workspace</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
<title>Admin — Next Workspace</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/htmx.org@2.0.4/dist/ext/response-targets.js"></script>
<style>{ adminStyles() }</style>
@adminStyles()
</head>
<body>
<div class="app-container">
<nav class="sidebar">
<div class="sidebar-header">
<h1>NextWks</h1>
<span class="version">Admin</span>
<div class="layout">
<header class="topbar">
<div class="topbar-left">
<a href="/" class="back-btn" title="Back to Workspace">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
<span class="page-title">Admin</span>
</div>
<ul class="sidebar-nav">
<li>
<a href="/admin" class="nav-link">Dashboard</a>
</li>
<li>
<a href="/admin/users" class="nav-link">Users</a>
</li>
</ul>
<div class="sidebar-footer">
<span class="status-indicator" id="health-status">Connected</span>
</header>
<nav class="sidebar">
<div class="sidebar-nav">
<a href="/admin" class={ "nav-link" + activeClass(page, "dashboard") }>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
Dashboard
</a>
<a href="/admin/users" class={ "nav-link" + activeClass(page, "users") }>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
Users
</a>
</div>
</nav>
<main class="main-content" id="main-content">
<main class="content">
{ children... }
</main>
</div>
@ -38,135 +40,74 @@ templ BaseLayout(title string) {
</html>
}
func activeClass(current, target string) string {
if current == target {
return " active"
}
return ""
}
templ adminStyles() {
<style type="text/css">
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg: #0f172a;
--surface: #1e293b;
--surface-2: #334155;
--border: #475569;
--text: #f1f5f9;
--text-muted: #94a3b8;
--primary: #3b82f6;
--primary-hover: #2563eb;
--danger: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
--radius: 8px;
--bg:#0d1117;--surface:#161b22;--surface2:#21262d;--border:#30363d;
--text:#e6edf3;--text2:#8b949e;--blue:#58a6ff;--purple:#a371f7;--green:#3fb950;
--red:#f85149;--orange:#d2991d
}
html { font-size: 14px; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
min-height: 100vh;
}
.app-container { display: flex; min-height: 100vh; }
.sidebar {
width: 240px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 1rem;
flex-shrink: 0;
}
.sidebar-header { margin-bottom: 2rem; }
.sidebar-header h1 { font-size: 1.25rem; font-weight: 700; color: var(--primary); }
.sidebar-header .version { font-size: 0.75rem; color: var(--text-muted); }
.sidebar-nav { list-style: none; display: flex; flex-direction: column; gap: 0.25rem; }
.nav-link {
display: block;
padding: 0.625rem 0.75rem;
color: var(--text);
text-decoration: none;
border-radius: var(--radius);
transition: background 0.15s;
}
.nav-link:hover { background: var(--surface-2); }
.sidebar-footer { margin-top: auto; padding-top: 1rem; }
.status-indicator { font-size: 0.75rem; color: var(--success); }
.main-content { flex: 1; padding: 1.5rem; overflow-y: auto; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
margin-bottom: 1rem;
}
.card h2 { font-size: 1.125rem; margin-bottom: 1rem; }
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 0.75rem 0.5rem;
border-bottom: 1px solid var(--border);
}
th { color: var(--text-muted); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; }
.btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border: none;
border-radius: var(--radius);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.15s;
text-decoration: none;
}
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { opacity: 0.9; }
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.375rem; color: var(--text-muted); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
.form-input {
width: 100%;
padding: 0.625rem 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 0.875rem;
}
.form-input:focus { outline: none; border-color: var(--primary); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-success { background: rgba(34,197,94,0.15); color: var(--success); }
.badge-danger { background: rgba(239,68,68,0.15); color: var(--danger); }
.badge-warning { background: rgba(245,158,11,0.15); color: var(--warning); }
.alert {
padding: 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
}
.alert-success { background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.3); color: var(--success); }
.alert-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); color: var(--danger); }
.password-display {
font-family: monospace;
background: var(--bg);
padding: 0.5rem;
border-radius: var(--radius);
user-select: all;
font-size: 0.875rem;
word-break: break-all;
}
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mt-2 { margin-top: 1rem; }
.flex { display: flex; }
.flex-between { display: flex; justify-content: space-between; align-items: center; }
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.layout{display:flex;min-height:100vh}
.topbar{display:flex;align-items:center;padding:0 16px;height:52px;background:var(--surface);border-bottom:1px solid var(--border);position:fixed;top:0;left:0;right:0;z-index:100}
.topbar-left{display:flex;align-items:center;gap:10px}
.back-btn{color:var(--text2);text-decoration:none;display:flex;align-items:center;padding:4px;border-radius:8px}
.back-btn:hover{background:var(--surface2);color:var(--text)}
.page-title{font-weight:600;font-size:1.05rem}
.sidebar{width:200px;background:var(--surface);border-right:1px solid var(--border);padding:12px 8px;position:fixed;top:52px;left:0;bottom:0;overflow-y:auto}
.sidebar-nav{display:flex;flex-direction:column;gap:2px}
.nav-link{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:8px;text-decoration:none;color:var(--text2);font-size:0.9rem;font-weight:500;transition:all 0.1s}
.nav-link:hover{background:var(--surface2);color:var(--text)}
.nav-link.active{background:var(--surface2);color:var(--blue)}
.nav-link svg{flex-shrink:0}
.content{flex:1;margin-left:200px;margin-top:52px;padding:24px;max-width:900px}
/* Dashboard */
.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:24px}
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;text-align:center}
.stat-value{font-size:1.5rem;font-weight:700;color:var(--blue)}
.stat-label{font-size:0.75rem;color:var(--text2);margin-top:4px}
/* User Management */
.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
.section-header h2{font-size:1.15rem;font-weight:600}
.section-header h3{font-size:1rem;font-weight:600}
.form-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;margin-bottom:16px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.form-group{margin-bottom:12px}
.form-group label{display:block;font-size:0.75rem;font-weight:500;color:var(--text2);margin-bottom:4px;text-transform:uppercase}
.input{width:100%;padding:10px 12px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:0.9rem;outline:none}
.input:focus{border-color:var(--blue)}
.btn{display:inline-flex;align-items:center;padding:8px 16px;border-radius:8px;border:none;background:var(--blue);color:#fff;font-size:0.85rem;font-weight:500;cursor:pointer;text-decoration:none}
.btn:hover{opacity:0.9}
.btn-sm{display:inline-flex;align-items:center;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:0.78rem;cursor:pointer}
.btn-sm:hover{background:var(--surface2)}
.btn-sm.danger{color:var(--red);border-color:var(--red)}
.btn-sm.danger:hover{background:rgba(248,81,73,0.1)}
.table-toolbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.table-title{font-weight:600;font-size:0.9rem}
.user-table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px}
table{width:100%;border-collapse:collapse}
th,td{text-align:left;padding:10px 8px;border-bottom:1px solid var(--border);font-size:0.85rem}
th{color:var(--text2);font-weight:600;font-size:0.75rem;text-transform:uppercase}
.loading-row{text-align:center;color:var(--text2);padding:2rem}
.role-tag{display:inline-block;padding:2px 8px;border-radius:99px;font-size:0.7rem;font-weight:600;background:rgba(88,166,255,0.15);color:var(--blue)}
.status-dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;vertical-align:middle}
.status-dot.on{background:var(--green)}
.status-dot.off{background:var(--red)}
.alert{padding:12px;border-radius:8px;margin-bottom:8px;font-size:0.85rem}
.alert.success{background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);color:var(--green)}
.alert.error{background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);color:var(--red)}
.password-box{font-family:monospace;background:var(--bg);padding:10px;border-radius:6px;margin:8px 0;font-size:0.85rem;user-select:all;word-break:break-all}
.password-hint{font-size:0.75rem;color:var(--orange)}
@media(max-width:640px){.sidebar{display:none}.content{margin-left:0}.stats-grid{grid-template-columns:1fr 1fr}.form-row{grid-template-columns:1fr}}
</style>
}

File diff suppressed because one or more lines are too long

View file

@ -1,45 +1,24 @@
package templates
templ UserDashboard() {
<div class="flex-between mb-2">
<div class="section-header">
<h2>User Management</h2>
<button class="btn btn-primary"
hx-get="/admin/users/create-form"
hx-target="#form-container"
hx-swap="innerHTML">
+ Add User
</button>
<button class="btn" hx-get="/admin/users/create-form" hx-target="#form-container" hx-swap="innerHTML">+ Add User</button>
</div>
<div id="form-container" class="mb-2"></div>
<div id="form-container" style="margin-bottom:16px"></div>
<div class="card" id="user-table-container">
<div class="flex-between mb-1">
<h2>Users</h2>
<button class="btn btn-sm btn-primary"
hx-get="/admin/api/users"
hx-target="#user-table-body"
hx-swap="innerHTML"
hx-trigger="load, click">
Refresh
</button>
<div class="user-table-wrap">
<div class="table-toolbar">
<span class="table-title">All Users</span>
<button class="btn-sm" hx-get="/admin/api/users" hx-target="#user-table-body" hx-swap="innerHTML" hx-trigger="load, click">Refresh</button>
</div>
<table>
<thead>
<tr>
<th>Username</th>
<th>Display Name</th>
<th>Email</th>
<th>Groups</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="user-table-body"
hx-get="/admin/api/users"
hx-trigger="load"
hx-swap="innerHTML">
<tr><td colspan="6" style="text-align:center;color:var(--text-muted);padding:2rem;">Loading users...</td></tr>
<thead><tr>
<th>Username</th><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th>
</tr></thead>
<tbody id="user-table-body" hx-get="/admin/api/users" hx-trigger="load" hx-swap="innerHTML">
<tr><td colspan="6" class="loading-row">Loading users...</td></tr>
</tbody>
</table>
</div>
@ -51,119 +30,83 @@ templ UserRows(users []UserRow) {
<td><strong>{ u.Username }</strong></td>
<td>{ u.DisplayName }</td>
<td>{ u.Email }</td>
<td>{ u.Groups }</td>
<td><span class="role-tag">{ u.Role }</span></td>
<td>
if u.Disabled {
<span class="badge badge-danger">Disabled</span>
<span class="status-dot off"></span> Disabled
} else {
<span class="badge badge-success">Active</span>
<span class="status-dot on"></span> Active
}
</td>
<td>
<button class="btn btn-sm btn-danger"
hx-delete="/admin/api/users/{ u.Username }"
hx-confirm="Delete user { u.Username }?"
<button class="btn-sm danger"
hx-delete={ "/admin/api/users/" + u.Username }
hx-confirm={ "Delete " + u.Username + "?" }
hx-target="closest tr"
hx-swap="delete">
Delete
</button>
hx-swap="delete">Delete</button>
</td>
</tr>
}
}
templ CreateUserForm() {
<div class="card" id="create-form">
<div class="flex-between mb-1">
<h2>Create New User</h2>
<button class="btn btn-sm btn-danger"
hx-get="/admin/users/cancel-form"
hx-target="#form-container"
hx-swap="innerHTML">
Cancel
</button>
<div class="form-card" id="create-form">
<div class="section-header">
<h3>New User</h3>
<button class="btn-sm"
hx-get="/admin/users/cancel-form" hx-target="#form-container" hx-swap="innerHTML">Cancel</button>
</div>
<form hx-post="/admin/api/users"
hx-target="#form-container"
hx-swap="innerHTML">
<form hx-post="/admin/api/users" hx-target="#form-container" hx-swap="innerHTML">
<div class="form-row">
<div class="form-group">
<label>Username *</label>
<input type="text" name="username" class="form-input" required placeholder="e.g. jdoe"/>
<input type="text" name="username" class="input" required placeholder="jdoe"/>
</div>
<div class="form-group">
<label>Display Name</label>
<input type="text" name="display_name" class="form-input" placeholder="e.g. John Doe"/>
<input type="text" name="display_name" class="input" placeholder="John Doe"/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="form-input" placeholder="e.g. john@example.com"/>
<input type="email" name="email" class="input" placeholder="john@example.com"/>
</div>
<div class="form-group">
<label>Groups</label>
<input type="text" name="groups" class="form-input" placeholder="e.g. admins,users"/>
<input type="text" name="groups" class="input" placeholder="admins,users"/>
</div>
</div>
<button type="submit" class="btn btn-primary mt-2">Create User</button>
<button type="submit" class="btn" style="margin-top:8px;width:100%">Create User</button>
</form>
</div>
}
templ CreateUserSuccess(results []CreateUserResultRow) {
<div class="alert alert-success">
<strong>Users created successfully!</strong>
<div class="flex-between mt-2">
<span></span>
<button class="btn btn-sm btn-primary"
hx-get="/admin/users/create-form"
hx-target="#form-container"
hx-swap="innerHTML">
+ Add Another
</button>
</div>
</div>
for _, r := range results {
<div class="card">
<div class="flex-between">
<div>
<strong>{ r.Username }</strong>
if r.Error != "" {
<span class="badge badge-danger">Error</span>
<div class="alert error">{ r.Error }</div>
} else {
<span class="badge badge-success">Created</span>
}
</div>
</div>
if r.Error != "" {
<p class="mt-2" style="color:var(--danger);">{ r.Error }</p>
} else {
<div class="mt-2">
<label style="font-size:0.75rem;color:var(--text-muted);">Generated Password (save this now)</label>
<div class="password-display">{ r.GeneratedPassword }</div>
<div class="alert success">
<strong>{ r.Username }</strong> created
<div class="password-box">{ r.GeneratedPassword }</div>
<div class="password-hint">Save this — it won't be shown again</div>
</div>
}
</div>
}
<script type="text/javascript">
// Auto-refresh the user table after creation
setTimeout(function() {
htmx.trigger("#user-table-body", "click");
}, 500);
setTimeout(function() { htmx.trigger('#user-table-body', 'click'); }, 500);
</script>
}
// Data types for template rendering
type UserRow struct {
Username string
DisplayName string
Email string
Role string
Groups string
Disabled bool
}
type CreateUserResultRow struct {
Username string
GeneratedPassword string

View file

@ -29,7 +29,7 @@ func UserDashboard() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex-between mb-2\"><h2>User Management</h2><button class=\"btn btn-primary\" hx-get=\"/admin/users/create-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">+ Add User</button></div><div id=\"form-container\" class=\"mb-2\"></div><div class=\"card\" id=\"user-table-container\"><div class=\"flex-between mb-1\"><h2>Users</h2><button class=\"btn btn-sm btn-primary\" hx-get=\"/admin/api/users\" hx-target=\"#user-table-body\" hx-swap=\"innerHTML\" hx-trigger=\"load, click\">Refresh</button></div><table><thead><tr><th>Username</th><th>Display Name</th><th>Email</th><th>Groups</th><th>Status</th><th>Actions</th></tr></thead> <tbody id=\"user-table-body\" hx-get=\"/admin/api/users\" hx-trigger=\"load\" hx-swap=\"innerHTML\"><tr><td colspan=\"6\" style=\"text-align:center;color:var(--text-muted);padding:2rem;\">Loading users...</td></tr></tbody></table></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"section-header\"><h2>User Management</h2><button class=\"btn\" hx-get=\"/admin/users/create-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">+ Add User</button></div><div id=\"form-container\" style=\"margin-bottom:16px\"></div><div class=\"user-table-wrap\"><div class=\"table-toolbar\"><span class=\"table-title\">All Users</span> <button class=\"btn-sm\" hx-get=\"/admin/api/users\" hx-target=\"#user-table-body\" hx-swap=\"innerHTML\" hx-trigger=\"load, click\">Refresh</button></div><table><thead><tr><th>Username</th><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th></tr></thead> <tbody id=\"user-table-body\" hx-get=\"/admin/api/users\" hx-trigger=\"load\" hx-swap=\"innerHTML\"><tr><td colspan=\"6\" class=\"loading-row\">Loading users...</td></tr></tbody></table></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -66,7 +66,7 @@ func UserRows(users []UserRow) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 51, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 30, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -79,7 +79,7 @@ func UserRows(users []UserRow) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(u.DisplayName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 52, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 31, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@ -92,41 +92,67 @@ func UserRows(users []UserRow) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 53, Col: 16}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 32, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td><span class=\"role-tag\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(u.Groups)
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(u.Role)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 54, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 33, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if u.Disabled {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"badge badge-danger\">Disabled</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"status-dot off\"></span> Disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"badge badge-success\">Active</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"status-dot on\"></span> Active")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td><button class=\"btn btn-sm btn-danger\" hx-delete=\"/admin/api/users/{ u.Username }\" hx-confirm=\"Delete user { u.Username }?\" hx-target=\"closest tr\" hx-swap=\"delete\">Delete</button></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td><button class=\"btn-sm danger\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/api/users/" + u.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 43, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-confirm=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("Delete " + u.Username + "?")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 44, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"closest tr\" hx-swap=\"delete\">Delete</button></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -151,12 +177,12 @@ func CreateUserForm() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"card\" id=\"create-form\"><div class=\"flex-between mb-1\"><h2>Create New User</h2><button class=\"btn btn-sm btn-danger\" hx-get=\"/admin/users/cancel-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">Cancel</button></div><form hx-post=\"/admin/api/users\" hx-target=\"#form-container\" hx-swap=\"innerHTML\"><div class=\"form-row\"><div class=\"form-group\"><label>Username *</label> <input type=\"text\" name=\"username\" class=\"form-input\" required placeholder=\"e.g. jdoe\"></div><div class=\"form-group\"><label>Display Name</label> <input type=\"text\" name=\"display_name\" class=\"form-input\" placeholder=\"e.g. John Doe\"></div></div><div class=\"form-row\"><div class=\"form-group\"><label>Email</label> <input type=\"email\" name=\"email\" class=\"form-input\" placeholder=\"e.g. john@example.com\"></div><div class=\"form-group\"><label>Groups</label> <input type=\"text\" name=\"groups\" class=\"form-input\" placeholder=\"e.g. admins,users\"></div></div><button type=\"submit\" class=\"btn btn-primary mt-2\">Create User</button></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"form-card\" id=\"create-form\"><div class=\"section-header\"><h3>New User</h3><button class=\"btn-sm\" hx-get=\"/admin/users/cancel-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">Cancel</button></div><form hx-post=\"/admin/api/users\" hx-target=\"#form-container\" hx-swap=\"innerHTML\"><div class=\"form-row\"><div class=\"form-group\"><label>Username *</label> <input type=\"text\" name=\"username\" class=\"input\" required placeholder=\"jdoe\"></div><div class=\"form-group\"><label>Display Name</label> <input type=\"text\" name=\"display_name\" class=\"input\" placeholder=\"John Doe\"></div></div><div class=\"form-row\"><div class=\"form-group\"><label>Email</label> <input type=\"email\" name=\"email\" class=\"input\" placeholder=\"john@example.com\"></div><div class=\"form-group\"><label>Groups</label> <input type=\"text\" name=\"groups\" class=\"input\" placeholder=\"admins,users\"></div></div><button type=\"submit\" class=\"btn\" style=\"margin-top:8px;width:100%\">Create User</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -180,91 +206,64 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"alert alert-success\"><strong>Users created successfully!</strong><div class=\"flex-between mt-2\"><span></span> <button class=\"btn btn-sm btn-primary\" hx-get=\"/admin/users/create-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">+ Add Another</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, r := range results {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"card\"><div class=\"flex-between\"><div><strong>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(r.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 131, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if r.Error != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"badge badge-danger\">Error</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span class=\"badge badge-success\">Created</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if r.Error != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"mt-2\" style=\"color:var(--danger);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 140, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mt-2\"><label style=\"font-size:0.75rem;color:var(--text-muted);\">Generated Password (save this now)</label><div class=\"password-display\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"alert error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.GeneratedPassword)
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 144, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 88, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"alert success\"><strong>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 91, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</strong> created<div class=\"password-box\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.GeneratedPassword)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 92, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"password-hint\">Save this — it won't be shown again</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<script type=\"text/javascript\">\n\t\t// Auto-refresh the user table after creation\n\t\tsetTimeout(function() {\n\t\t\thtmx.trigger(\"#user-table-body\", \"click\");\n\t\t}, 500);\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script type=\"text/javascript\">\n\t\tsetTimeout(function() { htmx.trigger('#user-table-body', 'click'); }, 500);\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -272,16 +271,14 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component {
})
}
// Data types for template rendering
type UserRow struct {
Username string
DisplayName string
Email string
Role string
Groups string
Disabled bool
}
type CreateUserResultRow struct {
Username string
GeneratedPassword string

View file

@ -44,6 +44,7 @@ func UserToRow(u User) templates.UserRow {
Username: u.Username,
DisplayName: u.DisplayName,
Email: u.Email,
Role: u.Role,
Groups: u.Groups,
Disabled: u.Disabled,
}