appview: paginate pipelines index #518

closed
opened by ptr.pet targeting master from [deleted fork]: pipeline-paginated
Changed files
+115 -41
appview
db
issues
middleware
pages
templates
repo
pipelines
pagination
pipelines
+7
appview/db/db.go
··· 741 func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) } 742 func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) } 743 func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) } 744 745 func (f filter) Condition() string { 746 rv := reflect.ValueOf(f.arg) 747 kind := rv.Kind() 748 749 // if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)` 750 if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array { 751 if rv.Len() == 0 {
··· 741 func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) } 742 func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) } 743 func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) } 744 + func FilterBetween(key string, arg1, arg2 any) filter { 745 + return newFilter(key, "between", []any{arg1, arg2}) 746 + } 747 748 func (f filter) Condition() string { 749 rv := reflect.ValueOf(f.arg) 750 kind := rv.Kind() 751 752 + if f.cmp == "between" { 753 + return fmt.Sprintf("%s %s ? and ?", f.key, f.cmp) 754 + } 755 + 756 // if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)` 757 if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array { 758 if rv.Len() == 0 {
+34 -12
appview/db/pipeline.go
··· 305 } 306 307 query := fmt.Sprintf(` 308 select 309 p.id, 310 p.knot, ··· 313 p.repo_name, 314 p.sha, 315 p.created, 316 - t.id, 317 - t.kind, 318 - t.push_ref, 319 - t.push_new_sha, 320 - t.push_old_sha, 321 - t.pr_source_branch, 322 - t.pr_target_branch, 323 - t.pr_source_sha, 324 - t.pr_action 325 from 326 - pipelines p 327 - join 328 - triggers t ON p.trigger_id = t.id 329 %s 330 `, whereClause) 331
··· 305 } 306 307 query := fmt.Sprintf(` 308 + with ranked_pipelines as ( 309 + select 310 + p.id, 311 + p.knot, 312 + p.rkey, 313 + p.repo_owner, 314 + p.repo_name, 315 + p.sha, 316 + p.created, 317 + t.id, 318 + t.kind, 319 + t.push_ref, 320 + t.push_new_sha, 321 + t.push_old_sha, 322 + t.pr_source_branch, 323 + t.pr_target_branch, 324 + t.pr_source_sha, 325 + t.pr_action, 326 + row_number() over (order by p.created desc) as row_num 327 + from 328 + pipelines p 329 + join 330 + triggers t ON p.trigger_id = t.id 331 + ) 332 select 333 p.id, 334 p.knot, ··· 337 p.repo_name, 338 p.sha, 339 p.created, 340 + p.id, 341 + p.kind, 342 + p.push_ref, 343 + p.push_new_sha, 344 + p.push_old_sha, 345 + p.pr_source_branch, 346 + p.pr_target_branch, 347 + p.pr_source_sha, 348 + p.pr_action 349 from 350 + ranked_pipelines p 351 %s 352 `, whereClause) 353
+1 -1
appview/issues/issues.go
··· 594 page, ok := r.Context().Value("page").(pagination.Page) 595 if !ok { 596 log.Println("failed to get page") 597 - page = pagination.FirstPage() 598 } 599 600 user := rp.oauth.GetUser(r)
··· 594 page, ok := r.Context().Value("page").(pagination.Page) 595 if !ok { 596 log.Println("failed to get page") 597 + page = pagination.FirstPage(10) 598 } 599 600 user := rp.oauth.GetUser(r)
+2 -1
appview/issues/router.go
··· 5 6 "github.com/go-chi/chi/v5" 7 "tangled.sh/tangled.sh/core/appview/middleware" 8 ) 9 10 func (i *Issues) Router(mw *middleware.Middleware) http.Handler { 11 r := chi.NewRouter() 12 13 r.Route("/", func(r chi.Router) { 14 - r.With(middleware.Paginate).Get("/", i.RepoIssues) 15 r.Get("/{issue}", i.RepoSingleIssue) 16 17 r.Group(func(r chi.Router) {
··· 5 6 "github.com/go-chi/chi/v5" 7 "tangled.sh/tangled.sh/core/appview/middleware" 8 + "tangled.sh/tangled.sh/core/appview/pagination" 9 ) 10 11 func (i *Issues) Router(mw *middleware.Middleware) http.Handler { 12 r := chi.NewRouter() 13 14 r.Route("/", func(r chi.Router) { 15 + r.With(middleware.Paginate(pagination.FirstPage(10))).Get("/", i.RepoIssues) 16 r.Get("/{issue}", i.RepoSingleIssue) 17 18 r.Group(func(r chi.Router) {
+24 -22
appview/middleware/middleware.go
··· 81 } 82 } 83 84 - func Paginate(next http.Handler) http.Handler { 85 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 - page := pagination.FirstPage() 87 88 - offsetVal := r.URL.Query().Get("offset") 89 - if offsetVal != "" { 90 - offset, err := strconv.Atoi(offsetVal) 91 - if err != nil { 92 - log.Println("invalid offset") 93 - } else { 94 - page.Offset = offset 95 } 96 - } 97 98 - limitVal := r.URL.Query().Get("limit") 99 - if limitVal != "" { 100 - limit, err := strconv.Atoi(limitVal) 101 - if err != nil { 102 - log.Println("invalid limit") 103 - } else { 104 - page.Limit = limit 105 } 106 - } 107 108 - ctx := context.WithValue(r.Context(), "page", page) 109 - next.ServeHTTP(w, r.WithContext(ctx)) 110 - }) 111 } 112 113 func (mw Middleware) knotRoleMiddleware(group string) middlewareFunc {
··· 81 } 82 } 83 84 + func Paginate(firstPage pagination.Page) func(next http.Handler) http.Handler { 85 + return func(next http.Handler) http.Handler { 86 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 + page := firstPage 88 89 + offsetVal := r.URL.Query().Get("offset") 90 + if offsetVal != "" { 91 + offset, err := strconv.Atoi(offsetVal) 92 + if err != nil { 93 + log.Println("invalid offset") 94 + } else { 95 + page.Offset = offset 96 + } 97 } 98 99 + limitVal := r.URL.Query().Get("limit") 100 + if limitVal != "" { 101 + limit, err := strconv.Atoi(limitVal) 102 + if err != nil { 103 + log.Println("invalid limit") 104 + } else { 105 + page.Limit = limit 106 + } 107 } 108 109 + ctx := context.WithValue(r.Context(), "page", page) 110 + next.ServeHTTP(w, r.WithContext(ctx)) 111 + }) 112 + } 113 } 114 115 func (mw Middleware) knotRoleMiddleware(group string) middlewareFunc {
+1
appview/pages/pages.go
··· 1094 LoggedInUser *oauth.User 1095 RepoInfo repoinfo.RepoInfo 1096 Pipelines []db.Pipeline 1097 Active string 1098 } 1099
··· 1094 LoggedInUser *oauth.User 1095 RepoInfo repoinfo.RepoInfo 1096 Pipelines []db.Pipeline 1097 + Page pagination.Page 1098 Active string 1099 } 1100
+32 -1
appview/pages/templates/repo/pipelines/pipelines.html
··· 7 {{ end }} 8 9 {{ define "repoContent" }} 10 - <div class="flex justify-between items-center gap-4"> 11 <div class="w-full flex flex-col gap-2"> 12 {{ range .Pipelines }} 13 {{ block "pipeline" (list $ .) }} {{ end }} ··· 17 </p> 18 {{ end }} 19 </div> 20 </div> 21 {{ end }} 22 ··· 100 </div> 101 {{ end }} 102 {{ end }}
··· 7 {{ end }} 8 9 {{ define "repoContent" }} 10 + <div class="flex flex-col justify-between items-center gap-4"> 11 <div class="w-full flex flex-col gap-2"> 12 {{ range .Pipelines }} 13 {{ block "pipeline" (list $ .) }} {{ end }} ··· 17 </p> 18 {{ end }} 19 </div> 20 + {{ block "pagination" . }} {{ end }} 21 </div> 22 {{ end }} 23 ··· 101 </div> 102 {{ end }} 103 {{ end }} 104 + 105 + {{ define "pagination" }} 106 + <div class="flex place-self-end mt-4 gap-2"> 107 + {{ if gt .Page.Offset 0 }} 108 + {{ $prev := .Page.Previous }} 109 + <a 110 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 111 + hx-boost="true" 112 + href = "/{{ $.RepoInfo.FullName }}/pipelines?&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 113 + > 114 + {{ i "chevron-left" "w-4 h-4" }} 115 + previous 116 + </a> 117 + {{ else }} 118 + <div></div> 119 + {{ end }} 120 + 121 + {{ if eq (len .Pipelines) .Page.Limit }} 122 + {{ $next := .Page.Next }} 123 + <a 124 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 125 + hx-boost="true" 126 + href = "/{{ $.RepoInfo.FullName }}/pipelines?offset={{ $next.Offset }}&limit={{ $next.Limit }}" 127 + > 128 + next 129 + {{ i "chevron-right" "w-4 h-4" }} 130 + </a> 131 + {{ end }} 132 + </div> 133 + {{ end }}
+3 -3
appview/pagination/page.go
··· 5 Limit int // number of items in a page 6 } 7 8 - func FirstPage() Page { 9 return Page{ 10 Offset: 0, 11 - Limit: 10, 12 } 13 } 14 15 func (p Page) Previous() Page { 16 if p.Offset-p.Limit < 0 { 17 - return FirstPage() 18 } else { 19 return Page{ 20 Offset: p.Offset - p.Limit,
··· 5 Limit int // number of items in a page 6 } 7 8 + func FirstPage(limit int) Page { 9 return Page{ 10 Offset: 0, 11 + Limit: limit, 12 } 13 } 14 15 func (p Page) Previous() Page { 16 if p.Offset-p.Limit < 0 { 17 + return FirstPage(p.Limit) 18 } else { 19 return Page{ 20 Offset: p.Offset - p.Limit,
+9
appview/pipelines/pipelines.go
··· 13 "tangled.sh/tangled.sh/core/appview/db" 14 "tangled.sh/tangled.sh/core/appview/oauth" 15 "tangled.sh/tangled.sh/core/appview/pages" 16 "tangled.sh/tangled.sh/core/appview/reporesolver" 17 "tangled.sh/tangled.sh/core/eventconsumer" 18 "tangled.sh/tangled.sh/core/idresolver" ··· 70 return 71 } 72 73 repoInfo := f.RepoInfo(user) 74 75 ps, err := db.GetPipelineStatuses( ··· 77 db.FilterEq("repo_owner", repoInfo.OwnerDid), 78 db.FilterEq("repo_name", repoInfo.Name), 79 db.FilterEq("knot", repoInfo.Knot), 80 ) 81 if err != nil { 82 l.Error("failed to query db", "err", err) ··· 86 p.pages.Pipelines(w, pages.PipelinesParams{ 87 LoggedInUser: user, 88 RepoInfo: repoInfo, 89 Pipelines: ps, 90 }) 91 }
··· 13 "tangled.sh/tangled.sh/core/appview/db" 14 "tangled.sh/tangled.sh/core/appview/oauth" 15 "tangled.sh/tangled.sh/core/appview/pages" 16 + "tangled.sh/tangled.sh/core/appview/pagination" 17 "tangled.sh/tangled.sh/core/appview/reporesolver" 18 "tangled.sh/tangled.sh/core/eventconsumer" 19 "tangled.sh/tangled.sh/core/idresolver" ··· 71 return 72 } 73 74 + page, ok := r.Context().Value("page").(pagination.Page) 75 + if !ok { 76 + l.Error("failed to get page") 77 + page = pagination.FirstPage(16) 78 + } 79 + 80 repoInfo := f.RepoInfo(user) 81 82 ps, err := db.GetPipelineStatuses( ··· 84 db.FilterEq("repo_owner", repoInfo.OwnerDid), 85 db.FilterEq("repo_name", repoInfo.Name), 86 db.FilterEq("knot", repoInfo.Knot), 87 + db.FilterBetween("row_num", page.Offset+1, page.Offset+page.Limit), 88 ) 89 if err != nil { 90 l.Error("failed to query db", "err", err) ··· 94 p.pages.Pipelines(w, pages.PipelinesParams{ 95 LoggedInUser: user, 96 RepoInfo: repoInfo, 97 + Page: page, 98 Pipelines: ps, 99 }) 100 }
+2 -1
appview/pipelines/router.go
··· 5 6 "github.com/go-chi/chi/v5" 7 "tangled.sh/tangled.sh/core/appview/middleware" 8 ) 9 10 func (p *Pipelines) Router(mw *middleware.Middleware) http.Handler { 11 r := chi.NewRouter() 12 - r.Get("/", p.Index) 13 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 14 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 15
··· 5 6 "github.com/go-chi/chi/v5" 7 "tangled.sh/tangled.sh/core/appview/middleware" 8 + "tangled.sh/tangled.sh/core/appview/pagination" 9 ) 10 11 func (p *Pipelines) Router(mw *middleware.Middleware) http.Handler { 12 r := chi.NewRouter() 13 + r.With(middleware.Paginate(pagination.FirstPage(16))).Get("/", p.Index) 14 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 15 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 16