loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

User details page (#26713)

This PR implements a proposal to clean up the admin users table by
moving some information out to a separate user details page (which also
displays some additional information).

Other changes:
- move edit user page from `/admin/users/{id}` to
`/admin/users/{id}/edit` -> `/admin/users/{id}` now shows the user
details page
- show if user is instance administrator as a label instead of a
separate column
- separate explore users template into a page- and a shared one, to make
it possible to use it on the user details page
- fix issue where there was no margin between alert message and
following content on admin pages

<details>

<summary>Screenshots</summary>


![grafik](https://github.com/go-gitea/gitea/assets/47871822/1ad57ac9-f20a-45a4-8477-ffe572a41e9e)


![grafik](https://github.com/go-gitea/gitea/assets/47871822/25786ecd-cb9d-4c92-90f4-e7f4292c073b)


</details>

Partially resolves #25939

---------

Co-authored-by: Giteabot <teabot@gitea.io>

authored by

Denys Konovalov
Giteabot
and committed by
GitHub
5b5bb8d3 3d109861

+242 -43
+1
options/locale/locale_en-US.ini
··· 2823 2823 users.list_status_filter.not_prohibit_login = Allow Login 2824 2824 users.list_status_filter.is_2fa_enabled = 2FA Enabled 2825 2825 users.list_status_filter.not_2fa_enabled = 2FA Disabled 2826 + users.details = User Details 2826 2827 2827 2828 emails.email_manage_panel = User Email Management 2828 2829 emails.primary = Primary
+58
routers/web/admin/users.go
··· 13 13 "code.gitea.io/gitea/models" 14 14 "code.gitea.io/gitea/models/auth" 15 15 "code.gitea.io/gitea/models/db" 16 + org_model "code.gitea.io/gitea/models/organization" 17 + repo_model "code.gitea.io/gitea/models/repo" 16 18 system_model "code.gitea.io/gitea/models/system" 17 19 user_model "code.gitea.io/gitea/models/user" 18 20 "code.gitea.io/gitea/modules/auth/password" ··· 32 34 const ( 33 35 tplUsers base.TplName = "admin/user/list" 34 36 tplUserNew base.TplName = "admin/user/new" 37 + tplUserView base.TplName = "admin/user/view" 35 38 tplUserEdit base.TplName = "admin/user/edit" 36 39 ) 37 40 ··· 247 250 ctx.Data["TwoFactorEnabled"] = hasTOTP || hasWebAuthn 248 251 249 252 return u 253 + } 254 + 255 + func ViewUser(ctx *context.Context) { 256 + ctx.Data["Title"] = ctx.Tr("admin.users.details") 257 + ctx.Data["PageIsAdminUsers"] = true 258 + ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation 259 + ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations 260 + ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 261 + 262 + u := prepareUserInfo(ctx) 263 + if ctx.Written() { 264 + return 265 + } 266 + 267 + repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 268 + ListOptions: db.ListOptions{ 269 + ListAll: true, 270 + }, 271 + OwnerID: u.ID, 272 + OrderBy: db.SearchOrderByAlphabetically, 273 + Private: true, 274 + Collaborate: util.OptionalBoolFalse, 275 + }) 276 + if err != nil { 277 + ctx.ServerError("SearchRepository", err) 278 + return 279 + } 280 + 281 + ctx.Data["Repos"] = repos 282 + ctx.Data["ReposTotal"] = int(count) 283 + 284 + emails, err := user_model.GetEmailAddresses(ctx.Doer.ID) 285 + if err != nil { 286 + ctx.ServerError("GetEmailAddresses", err) 287 + return 288 + } 289 + ctx.Data["Emails"] = emails 290 + ctx.Data["EmailsTotal"] = len(emails) 291 + 292 + orgs, err := org_model.FindOrgs(org_model.FindOrgOptions{ 293 + ListOptions: db.ListOptions{ 294 + ListAll: true, 295 + }, 296 + UserID: u.ID, 297 + IncludePrivate: true, 298 + }) 299 + if err != nil { 300 + ctx.ServerError("FindOrgs", err) 301 + return 302 + } 303 + 304 + ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template 305 + ctx.Data["OrgsTotal"] = len(orgs) 306 + 307 + ctx.HTML(http.StatusOK, tplUserView) 250 308 } 251 309 252 310 // EditUser show editing user page
+2 -1
routers/web/web.go
··· 573 573 m.Group("/users", func() { 574 574 m.Get("", admin.Users) 575 575 m.Combo("/new").Get(admin.NewUser).Post(web.Bind(forms.AdminCreateUserForm{}), admin.NewUserPost) 576 - m.Combo("/{userid}").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost) 576 + m.Get("/{userid}", admin.ViewUser) 577 + m.Combo("/{userid}/edit").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost) 577 578 m.Post("/{userid}/delete", admin.DeleteUser) 578 579 m.Post("/{userid}/avatar", web.Bind(forms.AvatarForm{}), admin.AvatarPost) 579 580 m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
+1 -1
templates/admin/layout_head.tmpl
··· 1 1 {{template "base/head" .ctxData}} 2 2 <div role="main" aria-label="{{.ctxData.Title}}" class="page-content {{.pageClass}}"> 3 - <div class="ui container"> 3 + <div class="ui container gt-mb-4"> 4 4 {{template "base/alert" .ctxData}} 5 5 </div> 6 6 <div class="ui container flex-container">
+6 -7
templates/admin/user/list.tmpl
··· 68 68 </th> 69 69 <th>{{.locale.Tr "email"}}</th> 70 70 <th>{{.locale.Tr "admin.users.activated"}}</th> 71 - <th>{{.locale.Tr "admin.users.admin"}}</th> 72 71 <th>{{.locale.Tr "admin.users.restricted"}}</th> 73 72 <th>{{.locale.Tr "admin.users.2fa"}}</th> 74 - <th>{{.locale.Tr "admin.users.repos"}}</th> 75 73 <th>{{.locale.Tr "admin.users.created"}}</th> 76 74 <th data-sortt-asc="lastlogin" data-sortt-desc="reverselastlogin"> 77 75 {{.locale.Tr "admin.users.last_login"}} 78 76 {{SortArrow "lastlogin" "reverselastlogin" $.SortType false}} 79 77 </th> 80 - <th>{{.locale.Tr "admin.users.edit"}}</th> 81 78 </tr> 82 79 </thead> 83 80 <tbody> 84 81 {{range .Users}} 85 82 <tr> 86 83 <td>{{.ID}}</td> 87 - <td><a href="{{.HomeLink}}">{{.Name}}</a></td> 84 + <td> 85 + <a href="{{$.Link}}/{{.ID}}">{{.Name}}</a> 86 + {{if .IsAdmin}} 87 + <span class="ui basic label">{{$.locale.Tr "admin.users.admin"}}</span> 88 + {{end}} 89 + </td> 88 90 <td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td> 89 91 <td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> 90 - <td>{{if .IsAdmin}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> 91 92 <td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> 92 93 <td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> 93 - <td>{{.NumRepos}}</td> 94 94 <td>{{DateTime "short" .CreatedUnix}}</td> 95 95 {{if .LastLoginUnix}} 96 96 <td>{{DateTime "short" .LastLoginUnix}}</td> 97 97 {{else}} 98 98 <td><span>{{$.locale.Tr "admin.users.never_login"}}</span></td> 99 99 {{end}} 100 - <td><a href="{{$.Link}}/{{.ID}}">{{svg "octicon-pencil"}}</a></td> 101 100 </tr> 102 101 {{end}} 103 102 </tbody>
+48
templates/admin/user/view.tmpl
··· 1 + {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin view user")}} 2 + 3 + <div class="admin-setting-content"> 4 + <div class="admin-responsive-columns"> 5 + <div class="gt-f1"> 6 + <h4 class="ui top attached header"> 7 + {{.Title}} 8 + <div class="ui right"> 9 + <a class="ui primary tiny button" href="{{.Link}}/edit">{{ctx.Locale.Tr "admin.users.edit"}}</a> 10 + </div> 11 + </h4> 12 + <div class="ui attached segment"> 13 + {{template "admin/user/view_details" .}} 14 + </div> 15 + </div> 16 + <div class="gt-f1"> 17 + <h4 class="ui top attached header"> 18 + {{ctx.Locale.Tr "admin.emails"}} 19 + <div class="ui right"> 20 + {{.EmailsTotal}} 21 + </div> 22 + </h4> 23 + <div class="ui attached segment"> 24 + {{template "admin/user/view_emails" .}} 25 + </div> 26 + </div> 27 + </div> 28 + <h4 class="ui top attached header"> 29 + {{ctx.Locale.Tr "admin.repositories"}} 30 + <div class="ui right"> 31 + {{.ReposTotal}} 32 + </div> 33 + </h4> 34 + <div class="ui attached segment"> 35 + {{template "explore/repo_list" .}} 36 + </div> 37 + <h4 class="ui top attached header"> 38 + {{ctx.Locale.Tr "settings.organization"}} 39 + <div class="ui right"> 40 + {{.OrgsTotal}} 41 + </div> 42 + </h4> 43 + <div class="ui attached segment"> 44 + {{template "explore/user_list" .}} 45 + </div> 46 + </div> 47 + 48 + {{template "admin/layout_footer" .}}
+65
templates/admin/user/view_details.tmpl
··· 1 + <div class="flex-list"> 2 + <div class="flex-item"> 3 + <div class="flex-item-leading"> 4 + {{ctx.AvatarUtils.Avatar .User 48}} 5 + </div> 6 + <div class="flex-item-main"> 7 + <div class="flex-item-title"> 8 + {{template "shared/user/name" .User}} 9 + {{if .User.IsAdmin}} 10 + <span class="ui basic label">{{ctx.Locale.Tr "admin.users.admin"}}</span> 11 + {{end}} 12 + </div> 13 + <div class="flex-item-body"> 14 + <b>{{ctx.Locale.Tr "admin.users.auth_source"}}:</b> 15 + {{if eq .LoginSource.ID 0}} 16 + {{ctx.Locale.Tr "admin.users.local"}} 17 + {{else}} 18 + {{.LoginSource.Name}} 19 + {{end}} 20 + </div> 21 + <div class="flex-item-body"> 22 + <b>{{ctx.Locale.Tr "admin.users.activated"}}:</b> 23 + {{if .User.IsActive}} 24 + {{svg "octicon-check"}} 25 + {{else}} 26 + {{svg "octicon-x"}} 27 + {{end}} 28 + </div> 29 + <div class="flex-item-body"> 30 + <b>{{ctx.Locale.Tr "admin.users.restricted"}}:</b> 31 + {{if .User.IsRestricted}} 32 + {{svg "octicon-check"}} 33 + {{else}} 34 + {{svg "octicon-x"}} 35 + {{end}} 36 + </div> 37 + <div class="flex-item-body"> 38 + <b>{{ctx.Locale.Tr "settings.visibility"}}:</b> 39 + {{if .User.Visibility.IsLimited}}{{ctx.Locale.Tr "settings.visibility.limited"}}{{end}} 40 + {{if .User.Visibility.IsPrivate}}{{ctx.Locale.Tr "settings.visibility.private"}}{{end}} 41 + </div> 42 + <div class="flex-item-body"> 43 + <b>{{ctx.Locale.Tr "admin.users.2fa"}}:</b> 44 + {{if .TwoFactorEnabled}} 45 + <span class="text green">{{svg "octicon-check"}}</span> 46 + {{else}} 47 + {{svg "octicon-x"}} 48 + {{end}} 49 + </div> 50 + {{if .User.Location}} 51 + <div class="flex-item-body"> 52 + <span class="flex-text-inline">{{svg "octicon-location"}}{{.User.Location}}</span> 53 + </div> 54 + {{end}} 55 + {{if .User.Website}} 56 + <div class="flex-item-body"> 57 + <span class="flex-text-inline"> 58 + {{svg "octicon-link"}} 59 + <a target="_blank" href="{{.User.Website}}">{{.User.Website}}</a> 60 + </span> 61 + </div> 62 + {{end}} 63 + </div> 64 + </div> 65 + </div>
+19
templates/admin/user/view_emails.tmpl
··· 1 + <div class="flex-list"> 2 + {{range .Emails}} 3 + <div class="flex-item"> 4 + <div class="flex-item-main"> 5 + <div class="flex-text-block"> 6 + {{.Email}} 7 + {{if .IsPrimary}} 8 + <div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div> 9 + {{end}} 10 + {{if .IsActivated}} 11 + <div class="ui green label">{{ctx.Locale.Tr "settings.activated"}}</div> 12 + {{else}} 13 + <div class="ui label">{{ctx.Locale.Tr "settings.requires_activation"}}</div> 14 + {{end}} 15 + </div> 16 + </div> 17 + </div> 18 + {{end}} 19 + </div>
+31
templates/explore/user_list.tmpl
··· 1 + <div class="flex-list"> 2 + {{range .Users}} 3 + <div class="flex-item flex-item-center"> 4 + <div class="flex-item-leading"> 5 + {{ctx.AvatarUtils.Avatar . 48}} 6 + </div> 7 + <div class="flex-item-main"> 8 + <div class="flex-item-title"> 9 + {{template "shared/user/name" .}} 10 + {{if .Visibility.IsPrivate}} 11 + <span class="ui basic tiny label">{{ctx.Locale.Tr "repo.desc.private"}}</span> 12 + {{end}} 13 + </div> 14 + <div class="flex-item-body"> 15 + {{if .Location}} 16 + <span class="flex-text-inline">{{svg "octicon-location"}}{{.Location}}</span> 17 + {{end}} 18 + {{if and .Email (or (and $.ShowUserEmail $.IsSigned (not .KeepEmailPrivate)) $.PageIsAdminUsers)}} 19 + <span class="flex-text-inline"> 20 + {{svg "octicon-mail"}} 21 + <a href="mailto:{{.Email}}">{{.Email}}</a> 22 + </span> 23 + {{end}} 24 + <span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span> 25 + </div> 26 + </div> 27 + </div> 28 + {{else}} 29 + <div class="flex-item">{{ctx.Locale.Tr "explore.user_no_results"}}</div> 30 + {{end}} 31 + </div>
+1 -31
templates/explore/users.tmpl
··· 4 4 <div class="ui container"> 5 5 {{template "explore/search" .}} 6 6 7 - <div class="flex-list"> 8 - {{range .Users}} 9 - <div class="flex-item flex-item-center"> 10 - <div class="flex-item-leading"> 11 - {{ctx.AvatarUtils.Avatar . 48}} 12 - </div> 13 - <div class="flex-item-main"> 14 - <div class="flex-item-title"> 15 - {{template "shared/user/name" .}} 16 - {{if .Visibility.IsPrivate}} 17 - <span class="ui basic tiny label">{{$.locale.Tr "repo.desc.private"}}</span> 18 - {{end}} 19 - </div> 20 - <div class="flex-item-body"> 21 - {{if .Location}} 22 - <span class="flex-text-inline">{{svg "octicon-location"}}{{.Location}}</span> 23 - {{end}} 24 - {{if and $.ShowUserEmail .Email $.IsSigned (not .KeepEmailPrivate)}} 25 - <span class="flex-text-inline"> 26 - {{svg "octicon-mail"}} 27 - <a href="mailto:{{.Email}}" rel="nofollow">{{.Email}}</a> 28 - </span> 29 - {{end}} 30 - <span class="flex-text-inline">{{svg "octicon-calendar"}}{{$.locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span> 31 - </div> 32 - </div> 33 - </div> 34 - {{else}} 35 - <div class="flex-item">{{$.locale.Tr "explore.user_no_results"}}</div> 36 - {{end}} 37 - </div> 7 + {{template "explore/user_list" .}} 38 8 39 9 {{template "base/paginate" .}} 40 10 </div>
+3 -3
tests/integration/admin_user_test.go
··· 51 51 52 52 func makeRequest(t *testing.T, formData user_model.User, headerCode int) { 53 53 session := loginUser(t, "user1") 54 - csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID))) 55 - req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID)), map[string]string{ 54 + csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit") 55 + req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{ 56 56 "_csrf": csrf, 57 57 "user_name": formData.Name, 58 58 "login_name": formData.LoginName, ··· 72 72 73 73 session := loginUser(t, "user1") 74 74 75 - csrf := GetCSRF(t, session, "/admin/users/8") 75 + csrf := GetCSRF(t, session, "/admin/users/8/edit") 76 76 req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{ 77 77 "_csrf": csrf, 78 78 })
+7
web_src/css/admin.css
··· 42 42 .admin .table th { 43 43 white-space: nowrap; 44 44 } 45 + 46 + .admin-responsive-columns { 47 + display: flex; 48 + flex-wrap: wrap; 49 + gap: 1rem; 50 + margin-bottom: 1rem; 51 + }