Expand 8 hidden lines 9 9 use std:: path:: { Path as FsPath , PathBuf } ; 10 10 11 11 use axum:: Form ; 12 - use axum:: extract:: { Path , State } ; 12 + use axum:: extract:: { Path , Query , State } ; 13 13 use axum:: http:: { HeaderMap , Uri } ; 14 14 use axum:: response:: { IntoResponse , Redirect , Response } ; 15 15 use axum_extra:: extract:: cookie:: CookieJar ; Expand 44 hidden lines 60 60 ( jar, page ( & chrome, "Admin" , admin_shell ( active, content) ) ) . into_response ( ) 61 61 } 62 62 63 + 64 + const PAGE_SIZE : i64 = 20 ; 65 + 66 + 67 + # [ derive ( Debug , Default , Deserialize ) ] 68 + pub struct Pager { 69 + # [ serde ( default ) ] 70 + page: Option < i64 > , 71 + } 72 + 73 + impl Pager { 74 + 75 + fn resolve ( & self ) -> ( i64 , i64 ) { 76 + let page = self . page. unwrap_or ( 1 ) . max ( 1 ) ; 77 + ( page, ( page - 1 ) * PAGE_SIZE ) 78 + } 79 + } 80 + 81 + 82 + 83 + fn pager_nav ( base : & str , page : i64 , total : i64 ) -> Markup { 84 + let pages = ( ( total + PAGE_SIZE - 1 ) / PAGE_SIZE ) . max ( 1 ) ; 85 + html ! { 86 + @if pages > 1 { 87 + nav class="pagination" aria-label="Pagination" { 88 + @if page > 1 { 89 + a class="btn" href=( format ! ( "{base}?page={}" , page - 1 ) ) { "← Previous" } 90 + } @else { 91 + span class="btn disabled" aria-disabled="true" { "← Previous" } 92 + } 93 + span class="muted pagination-page" { "Page " ( page) " of " ( pages) } 94 + @if page < pages { 95 + a class="btn" href=( format ! ( "{base}?page={}" , page + 1 ) ) { "Next →" } 96 + } @else { 97 + span class="btn disabled" aria-disabled="true" { "Next →" } 98 + } 99 + } 100 + } 101 + } 102 + } 103 + 63 104 64 105 65 106 Expand 43 hidden lines 109 150 RequireAdmin ( user) : RequireAdmin , 110 151 jar : CookieJar , 111 152 uri : Uri , 153 + Query ( pager) : Query < Pager > , 112 154 ) -> AppResult < Response > { 113 - let all = state. store. list_users ( ) . await ?; 155 + let ( page, offset) = pager. resolve ( ) ; 156 + let total = state. store. admin_counts ( ) . await ?. users; 157 + let all = state. store. list_users_paged ( PAGE_SIZE , offset) . await ?; 158 + 159 + let everyone = state. store. list_users ( ) . await ?; 114 160 let ( _, chrome) = build_chrome ( 115 161 & state, 116 162 jar. clone ( ) , Expand 29 hidden lines 146 192 } 147 193 } 148 194 } 195 + ( pager_nav( "/admin/users" , page, total) ) 149 196 } 150 197 } 151 198 section class="listing" { Expand 6 hidden lines 158 205 input type ="email" name="email" placeholder="email" required; 159 206 input type ="password" name="password" placeholder="password (optional)" ; 160 207 } 161 - label class="checkbox" { input type ="checkbox" name="admin" value="1" ; " Administrator" } 162 - button class="btn btn-primary inline-btn" type ="submit" { "Create user" } 208 + p class="muted field-hint" { "Leaving the password blank creates an account that must be activated by the user (they'll need an invite/reset)." } 209 + div class="admin-form-actions" { 210 + label class="checkbox" { input type ="checkbox" name="admin" value="1" ; " Administrator" } 211 + button class="btn btn-primary inline-btn" type ="submit" { "Create user" } 212 + } 163 213 } 164 - p class="muted field-hint" { "Leaving the password blank creates an account that must be activated by the user (they'll need an invite/reset)." } 165 214 } 166 215 } 167 216 section class="listing" { Expand 3 hidden lines 171 220 input type ="hidden" name="_csrf" value=( csrf) ; 172 221 div class="admin-form-row" { 173 222 select name="user_id" aria-label="User" { 174 - @for u in & all { option value=( u. id) { ( u. username) } } 223 + @for u in & everyone { option value=( u. id) { ( u. username) } } 175 224 } 176 225 input type ="password" name="password" placeholder="new password" required minlength="8" ; 226 + button class="btn inline-btn" type ="submit" { "Set password" } 177 227 } 178 - button class="btn inline-btn" type ="submit" { "Set password" } 179 228 } 180 229 } 181 230 } Expand 183 hidden lines 365 414 RequireAdmin ( user) : RequireAdmin , 366 415 jar : CookieJar , 367 416 uri : Uri , 417 + Query ( pager) : Query < Pager > , 368 418 ) -> AppResult < Response > { 369 - let repos = state. store. list_repos ( ) . await ?; 419 + let ( page, offset) = pager. resolve ( ) ; 420 + let total = state. store. admin_counts ( ) . await ?. repos; 421 + let repos = state. store. list_repos_paged ( PAGE_SIZE , offset) . await ?; 370 422 371 423 let mut owners = std:: collections:: HashMap :: new ( ) ; 372 424 for r in & repos { Expand 30 hidden lines 403 455 } 404 456 } 405 457 } 458 + ( pager_nav( "/admin/repos" , page, total) ) 406 459 } 407 460 } 408 461 } ; Expand 24 hidden lines 433 486 RequireAdmin ( user) : RequireAdmin , 434 487 jar : CookieJar , 435 488 uri : Uri , 489 + Query ( pager) : Query < Pager > , 436 490 ) -> AppResult < Response > { 437 - let groups = state. store. list_groups ( ) . await ?; 491 + let ( page, offset) = pager. resolve ( ) ; 492 + let total = state. store. admin_counts ( ) . await ?. groups; 493 + let groups = state. store. list_groups_paged ( PAGE_SIZE , offset) . await ?; 438 494 let mut owners = std:: collections:: HashMap :: new ( ) ; 439 495 for g in & groups { 440 496 if !owners. contains_key ( & g. owner_id) Expand 27 hidden lines 468 524 } 469 525 } 470 526 } 527 + ( pager_nav ( "/admin/groups" , page, total) ) 471 528 p class="muted field-hint" { "Deleting a group ungroups its repositories (they are not deleted)." } 472 529 } 473 530 } Expand 61 hidden lines 535 592 } 536 593 form method="post" action="/admin/invites" { 537 594 input type ="hidden" name="_csrf" value=( csrf) ; 538 - div class="admin-form-row" { 595 + div class="admin-form-row admin-form-row-last" { 539 596 input type ="text" name="note" placeholder="note (optional, e.g. who it's for)" ; 597 + button class="btn btn-primary inline-btn" type ="submit" { "Create invite" } 540 598 } 541 - button class="btn btn-primary inline-btn" type ="submit" { "Create invite" } 542 599 } 543 600 } 544 601 } Expand 102 hidden lines 647 704 "These override the config file. Leaving a field at its default keeps the config value; each row can be reset." 648 705 } 649 706 div class="card" { 650 - form method="post" action="/admin/settings" { 707 + form method="post" action="/admin/settings" class="stacked-form" { 651 708 input type ="hidden" name="_csrf" value=( csrf) ; 652 - label for ="s-name" { "Instance name" } 653 - input type ="text" id="s-name" name="name" value=( name) ; 654 - label for ="s-desc" { "Description" } 655 - input type ="text" id="s-desc" name="description" value=( desc) ; 656 - label for ="s-vis" { "Default repository visibility" } 657 - select id="s-vis" name="default_visibility" { 658 - ( vis_opt ( "public" , "Public" ) ) 659 - ( vis_opt ( "internal" , "Internal" ) ) 660 - ( vis_opt ( "private" , "Private" ) ) 709 + div class="form-field" { 710 + label for ="s-name" { "Instance name" } 711 + input type ="text" id="s-name" name="name" value=( name) ; 712 + } 713 + div class="form-field" { 714 + label for ="s-desc" { "Description" } 715 + input type ="text" id="s-desc" name="description" value=( desc) ; 716 + } 717 + div class="form-field" { 718 + label for ="s-vis" { "Default repository visibility" } 719 + select id="s-vis" name="default_visibility" { 720 + ( vis_opt ( "public" , "Public" ) ) 721 + ( vis_opt ( "internal" , "Internal" ) ) 722 + ( vis_opt ( "private" , "Private" ) ) 723 + } 724 + } 725 + div class="admin-form-actions" { 726 + label class="checkbox" { input type ="checkbox" name="allow_registration" value="1" checked[ reg] ; " Allow self-registration" } 727 + label class="checkbox" { input type ="checkbox" name="allow_anonymous" value="1" checked[ anon] ; " Allow anonymous browse/clone" } 728 + button class="btn btn-primary inline-btn" type ="submit" { "Save settings" } 661 729 } 662 - label class="checkbox" { input type ="checkbox" name="allow_registration" value="1" checked[ reg] ; " Allow self-registration" } 663 - label class="checkbox" { input type ="checkbox" name="allow_anonymous" value="1" checked[ anon] ; " Allow anonymous browse/clone" } 664 - button class="btn btn-primary inline-btn" type ="submit" { "Save settings" } 665 730 } 666 731 } 667 732 } ;