From b6e4345316b771f4a47aa0de2e439589c6d3bd4a Mon Sep 17 00:00:00 2001 From: cclohmar Date: Mon, 15 Jun 2026 05:23:46 +0000 Subject: [PATCH] feat(admin): split-pane user management with list and detail form --- src/core/admin/templates/layout.templ | 19 ++ src/core/admin/templates/layout_templ.go | 2 +- src/core/admin/templates/user-dashboard.templ | 135 ++++++----- .../admin/templates/user-dashboard_templ.go | 218 ++++++++++-------- src/core/admin/ui.go | 2 +- 5 files changed, 215 insertions(+), 161 deletions(-) diff --git a/src/core/admin/templates/layout.templ b/src/core/admin/templates/layout.templ index 049fa14..9bb42be 100644 --- a/src/core/admin/templates/layout.templ +++ b/src/core/admin/templates/layout.templ @@ -113,5 +113,24 @@ templ adminStyles() { .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}} + + /* Split pane */ + .split-pane{display:flex;flex-direction:column;gap:16px;height:calc(100vh - 100px)} + .pane-top{flex:1;overflow-y:auto;min-height:200px} + .pane-bottom{flex:1;overflow-y:auto;border-top:1px solid var(--border);padding-top:16px} + .user-list{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden} + .list-body{max-height:300px;overflow-y:auto} + .user-row{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background 0.1s} + .user-row:hover{background:var(--surface2)} + .user-row:last-child{border-bottom:none} + .user-row-left{display:flex;align-items:center;gap:10px} + .user-avatar{width:32px;height:32px;border-radius:50%;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:0.8rem;font-weight:600;color:var(--text2)} + .user-sub{font-size:0.75rem;color:var(--text2);margin-top:1px} + .user-row-right{display:flex;align-items:center;gap:8px} + .empty-row{padding:20px;text-align:center;color:var(--text2)} + .loading{padding:20px;text-align:center;color:var(--text2)} + .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--text2);gap:10px} + .empty-state p{font-size:0.85rem} + .detail-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px} } diff --git a/src/core/admin/templates/layout_templ.go b/src/core/admin/templates/layout_templ.go index bb8d16f..46bb52c 100644 --- a/src/core/admin/templates/layout_templ.go +++ b/src/core/admin/templates/layout_templ.go @@ -147,7 +147,7 @@ func adminStyles() templ.Component { templ_7745c5c3_Var8 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/src/core/admin/templates/user-dashboard.templ b/src/core/admin/templates/user-dashboard.templ index b242eca..3c7c3ea 100644 --- a/src/core/admin/templates/user-dashboard.templ +++ b/src/core/admin/templates/user-dashboard.templ @@ -1,83 +1,92 @@ package templates templ UserDashboard() { -
-

User Management

- -
- -
- -
-
- All Users - + @Layout("users") { +
+
+
+

Users

+ +
+
+
+ { 0 } users + +
+
+
Loading users...
+
+
+
+
+
+ +

Select a user or click + Add

+
+
- - - - - - - -
UsernameNameEmailRoleStatus
Loading users...
-
+ } } -templ UserRows(users []UserRow) { - for _, u := range users { - - { u.Username } - { u.DisplayName } - { u.Email } - { u.Role } - - if u.Disabled { - Disabled - } else { - Active - } - - - - - +templ UserList(users []UserRow) { + if len(users) == 0 { +
No users found
+ } else { + for _, u := range users { +
+
+
{ initials(u.DisplayName) }
+
+ { u.Username } +
{ u.Email }
+
+
+
+ { u.Role } + if u.Disabled { + + } else { + + } +
+
+ } } } templ CreateUserForm() { -
+

New User

- +
-
-
-
- - -
-
- - -
+ +
+ + +
+
+ + +
+
+ +
- - + +
- +
} @@ -90,15 +99,20 @@ templ CreateUserSuccess(results []CreateUserResultRow) {
{ r.Username } created
{ r.GeneratedPassword }
-
Save this — it won't be shown again
+
Save this password — it won't be shown again
} } } +func initials(s string) string { + if len(s) == 0 { return "?" } + return string(s[0]) +} + type UserRow struct { Username string DisplayName string @@ -106,6 +120,7 @@ type UserRow struct { Role string Groups string Disabled bool + Password string } type CreateUserResultRow struct { Username string diff --git a/src/core/admin/templates/user-dashboard_templ.go b/src/core/admin/templates/user-dashboard_templ.go index 87338bf..c0387f0 100644 --- a/src/core/admin/templates/user-dashboard_templ.go +++ b/src/core/admin/templates/user-dashboard_templ.go @@ -29,7 +29,38 @@ func UserDashboard() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

User Management

All Users
UsernameNameEmailRoleStatus
Loading users...
") + 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, "

Users

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(0) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 13, Col: 29} + } + _, 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, " users
Loading users...

Select a user or click + Add

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = Layout("users").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -37,7 +68,7 @@ func UserDashboard() templ.Component { }) } -func UserRows(users []UserRow) templ.Component { +func UserList(users []UserRow) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -53,108 +84,89 @@ func UserRows(users []UserRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var2 := templ.GetChildren(ctx) - if templ_7745c5c3_Var2 == nil { - templ_7745c5c3_Var2 = templ.NopComponent + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - for _, u := range users { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if len(users) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
No users found
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - 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: 30, Col: 27} - } - _, 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, 3, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - 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: 31, Col: 22} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - 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: 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, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 string - 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: 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, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if u.Disabled { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " Disabled") + } else { + for _, u := range users { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " Active") + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(initials(u.DisplayName)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 38, Col: 55} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, 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: 40, Col: 26} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, 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: 41, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, 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: 45, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if u.Disabled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err } } return nil @@ -182,7 +194,7 @@ func CreateUserForm() templ.Component { templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

New User

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

New User

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -220,7 +232,7 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component { var templ_7745c5c3_Var11 string 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: 88, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 97, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -238,7 +250,7 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component { 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 100, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -251,19 +263,19 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component { 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 101, 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, "
Save this — it won't be shown again
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Save this password — it won't be shown again
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -271,6 +283,13 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component { }) } +func initials(s string) string { + if len(s) == 0 { + return "?" + } + return string(s[0]) +} + type UserRow struct { Username string DisplayName string @@ -278,6 +297,7 @@ type UserRow struct { Role string Groups string Disabled bool + Password string } type CreateUserResultRow struct { Username string diff --git a/src/core/admin/ui.go b/src/core/admin/ui.go index e95d40f..85c714a 100644 --- a/src/core/admin/ui.go +++ b/src/core/admin/ui.go @@ -224,7 +224,7 @@ func (h *Handler) userRowsHandler(w http.ResponseWriter, r *http.Request) { rows = append(rows, UserToRow(u)) } - component := templates.UserRows(rows) + component := templates.UserList(rows) component.Render(r.Context(), w) }