package httpd
import (
"bytes"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
"gitbay.org/gitbay/internal/control"
"gitbay.org/gitbay/internal/protocol"
"gitbay.org/gitbay/internal/store"
"gitbay.org/gitbay/internal/web"
)
// buildFilter is the builds page's GET filter: branch, status and job,
// each optional and independent (#224).
type buildFilter struct {
Ref string
Status string
Job string
}
// buildFilterLink is one of the links buildFacets splits into the side
// column's Status and Jobs groups: a status or a job, with the other two
// parameters carried along so clicking one never drops another.
type buildFilterLink struct {
Label string
Href string
Active bool
}
// buildStatuses is the fixed vocabulary a build's status takes, in the
// order the Status group offers them. Its length is also where buildFacets
// cuts filterLinks' rows apart.
var buildStatuses = []string{"pending", "running", "success", "failure", "cancelled"}
// filterLinks builds the status and job rows: "all" (clears status and
// job), one link per status, and one per job the repository's CI config
// names. Each link keeps the filter's other two parameters and net/url encodes
// them, so a branch name or job name with an odd character does not break
// the query string it lands in.
func filterLinks(f buildFilter, jobs []control.JobOut) []buildFilterLink {
href := func(status, job string) string {
q := url.Values{}
if f.Ref != "" {
q.Set("ref", f.Ref)
}
if status != "" {
q.Set("status", status)
}
if job != "" {
q.Set("job", job)
}
return "?" + q.Encode()
}
links := []buildFilterLink{
{Label: "all", Href: href("", ""), Active: f.Status == "" && f.Job == ""},
}
for _, s := range buildStatuses {
links = append(links, buildFilterLink{Label: s, Href: href(s, f.Job), Active: f.Status == s})
}
for _, j := range jobs {
links = append(links, buildFilterLink{Label: j.Name, Href: href(f.Status, j.Name), Active: f.Job == j.Name})
}
return links
}
// maxBranchFacets caps the Branches group, the way topicFacets caps
// topics: a page of builds names as many refs as it likes and the column
// is not a branch listing. The field below the column takes any ref.
const maxBranchFacets = 10
// buildFacets is the builds page's side column: filterLinks' rows split
// into their groups, plus one link per branch seen, which keeps status
// and job and clears itself when active.
func buildFacets(f buildFilter, jobs []control.JobOut, refs []string) []facetGroup {
links := filterLinks(f, jobs)
n := 1 + len(buildStatuses)
status := facetGroup{Title: "Status"}
for _, l := range links[:n] {
status.Items = append(status.Items, facetItem{Label: l.Label, Href: l.Href, Active: l.Active})
}
job := facetGroup{Title: "Jobs"}
for _, l := range links[n:] {
job.Items = append(job.Items, facetItem{Label: l.Label, Href: l.Href, Active: l.Active})
}
branch := facetGroup{Title: "Branches"}
base := url.Values{"ref": {f.Ref}, "status": {f.Status}, "job": {f.Job}}
shown := refs
if len(shown) > maxBranchFacets {
shown = shown[:maxBranchFacets]
// the ref in force belongs in the group wherever it sits, or the
// filter it set cannot be cleared from the column. The reslice
// caps the capacity so the append copies instead of writing
// through to refs.
if i := slices.Index(refs, f.Ref); i >= maxBranchFacets {
shown = append(shown[:maxBranchFacets-1:maxBranchFacets-1], refs[i])
}
}
for _, ref := range shown {
active := ref == f.Ref
href := facetHref(base, "ref", ref)
if active {
href = facetHref(base, "ref", "")
}
branch.Items = append(branch.Items, facetItem{Label: ref, Href: href, Active: active})
}
return []facetGroup{status, job, branch}
}
// distinctRefs lists each ref among builds once, in order, plus the
// current filter value if it is not already there. It backs the branch
// field's