appview: implement pagination for pipelines index #521

closed
opened by ptr.pet targeting master from ptr.pet/core: pipeline-paginated
+2 -2
appview/db/artifact.go
···
return err
}
-
func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) {
+
func GetArtifact(e Execer, filters ...Filter) ([]Artifact, error) {
var artifacts []Artifact
var conditions []string
···
return artifacts, nil
}
-
func DeleteArtifact(e Execer, filters ...filter) error {
+
func DeleteArtifact(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+1 -1
appview/db/collaborators.go
···
return err
}
-
func DeleteCollaborator(e Execer, filters ...filter) error {
+
func DeleteCollaborator(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+13 -13
appview/db/db.go
···
return d.DB.Close()
}
-
type filter struct {
+
type Filter struct {
key string
arg any
cmp string
}
-
func newFilter(key, cmp string, arg any) filter {
-
return filter{
+
func newFilter(key, cmp string, arg any) Filter {
+
return Filter{
key: key,
arg: arg,
cmp: cmp,
}
}
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
-
func FilterBetween(key string, arg1, arg2 any) filter {
+
func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
+
func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
+
func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
+
func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
+
func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
+
func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
+
func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
+
func FilterBetween(key string, arg1, arg2 any) Filter {
return newFilter(key, "between", []any{arg1, arg2})
}
-
func (f filter) Condition() string {
+
func (f Filter) Condition() string {
rv := reflect.ValueOf(f.arg)
kind := rv.Kind()
···
return fmt.Sprintf("%s %s ?", f.key, f.cmp)
}
-
func (f filter) Arg() []any {
+
func (f Filter) Arg() []any {
rv := reflect.ValueOf(f.arg)
kind := rv.Kind()
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
+1 -1
appview/db/follow.go
···
return result, nil
}
-
func GetFollows(e Execer, limit int, filters ...filter) ([]Follow, error) {
+
func GetFollows(e Execer, limit int, filters ...Filter) ([]Follow, error) {
var follows []Follow
var conditions []string
+7 -7
appview/db/issues.go
···
return err
}
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]Issue, error) {
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...Filter) ([]Issue, error) {
issueMap := make(map[string]*Issue) // at-uri -> issue
var conditions []string
···
return issues, nil
}
-
func GetIssues(e Execer, filters ...filter) ([]Issue, error) {
+
func GetIssues(e Execer, filters ...Filter) ([]Issue, error) {
return GetIssuesPaginated(e, pagination.Page{No: 0, Count: 10}, filters...)
}
···
return id, nil
}
-
func DeleteIssueComments(e Execer, filters ...filter) error {
+
func DeleteIssueComments(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func GetIssueComments(e Execer, filters ...filter) ([]IssueComment, error) {
+
func GetIssueComments(e Execer, filters ...Filter) ([]IssueComment, error) {
var comments []IssueComment
var conditions []string
···
return comments, nil
}
-
func DeleteIssues(e Execer, filters ...filter) error {
+
func DeleteIssues(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func CloseIssues(e Execer, filters ...filter) error {
+
func CloseIssues(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func ReopenIssues(e Execer, filters ...filter) error {
+
func ReopenIssues(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+1 -1
appview/db/language.go
···
Bytes int64
}
-
func GetRepoLanguages(e Execer, filters ...filter) ([]RepoLanguage, error) {
+
func GetRepoLanguages(e Execer, filters ...Filter) ([]RepoLanguage, error) {
var conditions []string
var args []any
for _, filter := range filters {
+62 -14
appview/db/pipeline.go
···
ExitCode int
}
-
func GetPipelines(e Execer, filters ...filter) ([]Pipeline, error) {
+
func GetPipelines(e Execer, filters ...Filter) ([]Pipeline, error) {
var pipelines []Pipeline
var conditions []string
···
// this is a mega query, but the most useful one:
// get N pipelines, for each one get the latest status of its N workflows
-
func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) {
+
func GetPipelineStatuses(e Execer, filters ...Filter) ([]Pipeline, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
}
query := fmt.Sprintf(`
+
with ranked_pipelines as (
+
select
+
p.id,
+
p.knot,
+
p.rkey,
+
p.repo_owner,
+
p.repo_name,
+
p.sha,
+
p.created,
+
t.id,
+
t.kind,
+
t.push_ref,
+
t.push_new_sha,
+
t.push_old_sha,
+
t.pr_source_branch,
+
t.pr_target_branch,
+
t.pr_source_sha,
+
t.pr_action,
+
row_number() over (order by p.created desc) as row_num
+
from
+
pipelines p
+
join
+
triggers t ON p.trigger_id = t.id
+
)
select
p.id,
p.knot,
···
p.repo_name,
p.sha,
p.created,
-
t.id,
-
t.kind,
-
t.push_ref,
-
t.push_new_sha,
-
t.push_old_sha,
-
t.pr_source_branch,
-
t.pr_target_branch,
-
t.pr_source_sha,
-
t.pr_action
+
p.id,
+
p.kind,
+
p.push_ref,
+
p.push_new_sha,
+
p.push_old_sha,
+
p.pr_source_branch,
+
p.pr_target_branch,
+
p.pr_source_sha,
+
p.pr_action
from
-
pipelines p
-
join
-
triggers t ON p.trigger_id = t.id
+
ranked_pipelines p
%s
`, whereClause)
···
return all, nil
}
+
+
// get pipeline counts, implement with filters
+
func GetPipelineCount(e Execer, filters ...Filter) (int, error) {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
query := fmt.Sprintf(`select count(*) as count from pipelines %s`, whereClause)
+
+
row := e.QueryRow(query, args...)
+
+
var count int
+
if err := row.Scan(&count); err != nil {
+
return 0, err
+
}
+
+
return count, nil
+
}
+1 -1
appview/db/profile.go
···
return tx.Commit()
}
-
func GetProfiles(e Execer, filters ...filter) (map[string]*Profile, error) {
+
func GetProfiles(e Execer, filters ...Filter) (map[string]*Profile, error) {
var conditions []string
var args []any
for _, filter := range filters {
+4 -4
appview/db/pulls.go
···
return pullId - 1, err
}
-
func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*Pull, error) {
+
func GetPullsWithLimit(e Execer, limit int, filters ...Filter) ([]*Pull, error) {
pulls := make(map[int]*Pull)
var conditions []string
···
return orderedByPullId, nil
}
-
func GetPulls(e Execer, filters ...filter) ([]*Pull, error) {
+
func GetPulls(e Execer, filters ...Filter) ([]*Pull, error) {
return GetPullsWithLimit(e, 0, filters...)
}
···
return err
}
-
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
+
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...Filter) error {
var conditions []string
var args []any
···
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
// otherwise submissions are immutable
-
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
+
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...Filter) error {
var conditions []string
var args []any
+1 -1
appview/db/punchcard.go
···
Punches []Punch
}
-
func MakePunchcard(e Execer, filters ...filter) (*Punchcard, error) {
+
func MakePunchcard(e Execer, filters ...Filter) (*Punchcard, error) {
punchcard := &Punchcard{}
now := time.Now()
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+3 -3
appview/db/registration.go
···
NeedsUpgrade
)
-
func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) {
+
func GetRegistrations(e Execer, filters ...Filter) ([]Registration, error) {
var registrations []Registration
var conditions []string
···
return registrations, nil
}
-
func MarkRegistered(e Execer, filters ...filter) error {
+
func MarkRegistered(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func DeleteKnot(e Execer, filters ...filter) error {
+
func DeleteKnot(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+2 -2
appview/db/repos.go
···
return p
}
-
func GetRepos(e Execer, limit int, filters ...filter) ([]Repo, error) {
+
func GetRepos(e Execer, limit int, filters ...Filter) ([]Repo, error) {
repoMap := make(map[syntax.ATURI]*Repo)
var conditions []string
···
return repos, nil
}
-
func CountRepos(e Execer, filters ...filter) (int64, error) {
+
func CountRepos(e Execer, filters ...Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
+5 -5
appview/db/spindle.go
···
Created time.Time
}
-
func GetSpindles(e Execer, filters ...filter) ([]Spindle, error) {
+
func GetSpindles(e Execer, filters ...Filter) ([]Spindle, error) {
var spindles []Spindle
var conditions []string
···
return err
}
-
func VerifySpindle(e Execer, filters ...filter) (int64, error) {
+
func VerifySpindle(e Execer, filters ...Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return res.RowsAffected()
}
-
func DeleteSpindle(e Execer, filters ...filter) error {
+
func DeleteSpindle(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func RemoveSpindleMember(e Execer, filters ...filter) error {
+
func RemoveSpindleMember(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func GetSpindleMembers(e Execer, filters ...filter) ([]SpindleMember, error) {
+
func GetSpindleMembers(e Execer, filters ...Filter) ([]SpindleMember, error) {
var members []SpindleMember
var conditions []string
+2 -2
appview/db/star.go
···
}
}
-
func GetStars(e Execer, limit int, filters ...filter) ([]Star, error) {
+
func GetStars(e Execer, limit int, filters ...Filter) ([]Star, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return stars, nil
}
-
func CountStars(e Execer, filters ...filter) (int64, error) {
+
func CountStars(e Execer, filters ...Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
+3 -3
appview/db/strings.go
···
return err
}
-
func GetStrings(e Execer, limit int, filters ...filter) ([]String, error) {
+
func GetStrings(e Execer, limit int, filters ...Filter) ([]String, error) {
var all []String
var conditions []string
···
return all, nil
}
-
func CountStrings(e Execer, filters ...filter) (int64, error) {
+
func CountStrings(e Execer, filters ...Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return count, nil
}
-
func DeleteString(e Execer, filters ...filter) error {
+
func DeleteString(e Execer, filters ...Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+1
appview/pages/pages.go
···
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
Pipelines []db.Pipeline
+
Pagination pagination.Pagination
Active string
+33 -1
appview/pages/templates/repo/pipelines/pipelines.html
···
{{ end }}
{{ define "repoContent" }}
-
<div class="flex justify-between items-center gap-4">
+
<div class="flex flex-col justify-between items-center gap-4">
<div class="w-full flex flex-col gap-2">
{{ range .Pipelines }}
{{ block "pipeline" (list $ .) }} {{ end }}
···
</p>
{{ end }}
</div>
+
{{ block "pagination" . }} {{ end }}
</div>
{{ end }}
···
</div>
{{ end }}
{{ end }}
+
+
{{ define "pagination" }}
+
{{ $currentPage := .Pagination.CurrentPage }}
+
<div class="flex place-self-end mt-4 gap-2">
+
{{ if gt $currentPage.No 0 }}
+
{{ $prev := .Pagination.PreviousPage }}
+
<a
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
+
hx-boost="true"
+
href = "/{{ $.RepoInfo.FullName }}/pipelines?page={{ add $prev.No 1 }}&count={{ $prev.Count }}"
+
>
+
{{ i "chevron-left" "w-4 h-4" }}
+
previous
+
</a>
+
{{ else }}
+
<div></div>
+
{{ end }}
+
+
{{ if lt (add $currentPage.No 1) .Pagination.TotalPageCount }}
+
{{ $next := .Pagination.NextPage }}
+
<a
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
+
hx-boost="true"
+
href = "/{{ $.RepoInfo.FullName }}/pipelines?page={{ add $next.No 1 }}&count={{ $next.Count }}"
+
>
+
next
+
{{ i "chevron-right" "w-4 h-4" }}
+
</a>
+
{{ end }}
+
</div>
+
{{ end }}
+29 -3
appview/pipelines/pipelines.go
···
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
+
"tangled.sh/tangled.sh/core/appview/pagination"
"tangled.sh/tangled.sh/core/appview/reporesolver"
"tangled.sh/tangled.sh/core/eventconsumer"
"tangled.sh/tangled.sh/core/idresolver"
···
}
repoInfo := f.RepoInfo(user)
-
-
ps, err := db.GetPipelineStatuses(
-
p.db,
+
filters := []db.Filter{
db.FilterEq("repo_owner", repoInfo.OwnerDid),
db.FilterEq("repo_name", repoInfo.Name),
db.FilterEq("knot", repoInfo.Knot),
+
}
+
+
page, ok := r.Context().Value("page").(pagination.Page)
+
if !ok {
+
l.Error("failed to get page")
+
page = pagination.Page{No: 0, Count: 16}
+
}
+
pipelinesCount, err := db.GetPipelineCount(p.db, filters...)
+
if err != nil {
+
l.Error("failed to get pipeline count", "err", err)
+
p.pages.Notice(w, "pipelines", "Failed to load pipelines. Try again later.")
+
return
+
}
+
paginate := pagination.NewFromPage(page, pipelinesCount)
+
+
currentPage := paginate.CurrentPage()
+
filters = append(filters, db.FilterBetween(
+
"row_num",
+
currentPage.Count*currentPage.No+1,
+
currentPage.Count*(currentPage.No+1),
+
))
+
+
ps, err := db.GetPipelineStatuses(
+
p.db,
+
filters...,
)
if err != nil {
l.Error("failed to query db", "err", err)
+
p.pages.Notice(w, "pipelines", "Failed to load pipelines. Try again later.")
return
}
p.pages.Pipelines(w, pages.PipelinesParams{
LoggedInUser: user,
RepoInfo: repoInfo,
+
Pagination: paginate,
Pipelines: ps,
})
}
+1 -1
appview/pipelines/router.go
···
func (p *Pipelines) Router(mw *middleware.Middleware) http.Handler {
r := chi.NewRouter()
-
r.Get("/", p.Index)
+
r.With(middleware.Paginate(16)).Get("/", p.Index)
r.Get("/{pipeline}/workflow/{workflow}", p.Workflow)
r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs)