Commit 09bc1d3bb9

09bc1d3bb983e2b1bb897593aada03bf92ca1e36

parent: c8d6b79a83

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-19 21:43 UTC

web: the builds page filters in a side column

Ref #226
internal/httpd/buildpages_test.go +2 −1
@@ -35,11 +35,12 @@ func TestBuildsPageRendersCommandOutput(t *testing.T) {
3535 Runs []buildRun
3636 Filter buildFilter
3737 FilterLinks []buildFilterLink
38 Facets []facetGroup
3839 Refs []string
3940 CanWrite bool
4041 Notice string
4142 }{
42 testRepoPage(), builds, jobs, groupRuns(builds), filter, filterLinks(filter, jobs),
43 testRepoPage(), builds, jobs, groupRuns(builds), filter, filterLinks(filter, jobs), nil,
4344 distinctRefs(builds, filter.Ref), true, "",
4445 })
4546 if err != nil {
internal/httpd/builds.go +32 −1
@@ -60,6 +60,33 @@ func filterLinks(f buildFilter, jobs []control.JobOut) []buildFilterLink {
6060 return links
6161}
6262
63// buildFacets is the builds page's side column: filterLinks' rows split
64// into their groups, plus one link per branch seen, which keeps status
65// and job and clears itself when active.
66func buildFacets(f buildFilter, jobs []control.JobOut, refs []string) []facetGroup {
67 links := filterLinks(f, jobs)
68 n := 1 + len(buildStatuses)
69 status := facetGroup{Title: "Status"}
70 for _, l := range links[:n] {
71 status.Items = append(status.Items, facetItem{Label: l.Label, Href: l.Href, Active: l.Active})
72 }
73 job := facetGroup{Title: "Jobs"}
74 for _, l := range links[n:] {
75 job.Items = append(job.Items, facetItem{Label: l.Label, Href: l.Href, Active: l.Active})
76 }
77 branch := facetGroup{Title: "Branches"}
78 base := url.Values{"ref": {f.Ref}, "status": {f.Status}, "job": {f.Job}}
79 for _, ref := range refs {
80 active := ref == f.Ref
81 href := facetHref(base, "ref", ref)
82 if active {
83 href = facetHref(base, "ref", "")
84 }
85 branch.Items = append(branch.Items, facetItem{Label: ref, Href: href, Active: active})
86 }
87 return []facetGroup{status, job, branch}
88}
89
6390// distinctRefs lists each ref among builds once, in order, plus the
6491// current filter value if it is not already there. It backs the branch
6592// field's <datalist> suggestions, not a claim about what branches exist:
@@ -176,6 +203,8 @@ func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
176203 var jobs []control.JobOut
177204 s.runControlInto(viewer, []string{"build", "jobs", p.Repo.Path()}, &jobs)
178205
206 refs := distinctRefs(builds, filter.Ref)
207
179208 s.render(w, "builds.html", struct {
180209 repoPage
181210 Builds []control.BuildOut
@@ -183,10 +212,12 @@ func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
183212 Runs []buildRun
184213 Filter buildFilter
185214 FilterLinks []buildFilterLink
215 Facets []facetGroup
186216 Refs []string
187217 CanWrite bool
188218 Notice string
189 }{p, builds, jobs, groupRuns(builds), filter, filterLinks(filter, jobs), distinctRefs(builds, filter.Ref),
219 }{p, builds, jobs, groupRuns(builds), filter, filterLinks(filter, jobs),
220 buildFacets(filter, jobs, refs), refs,
190221 s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
191222}
192223
internal/httpd/builds_test.go +25
@@ -149,3 +149,28 @@ func TestDistinctRefsNoDuplicateWhenCurrentAlreadyPresent(t *testing.T) {
149149 t.Errorf("distinctRefs = %v, want %v", got, want)
150150 }
151151}
152
153// The builds column groups the same links filterLinks makes: "all" and
154// the statuses, then the jobs, then the branches seen (desktop layout spec).
155func TestBuildFacetsGroups(t *testing.T) {
156 f := buildFilter{Ref: "main", Status: "success"}
157 groups := buildFacets(f, []control.JobOut{{Name: "lint"}}, []string{"main", "dev"})
158 if len(groups) != 3 || groups[0].Title != "Status" || groups[1].Title != "Jobs" || groups[2].Title != "Branches" {
159 t.Fatalf("groups: %+v", groups)
160 }
161 if groups[0].Items[0].Label != "all" || groups[0].Items[0].Href != "?ref=main" || groups[0].Items[0].Active {
162 t.Errorf("all: %+v", groups[0].Items[0])
163 }
164 if s := groups[0].Items[3]; s.Label != "success" || !s.Active {
165 t.Errorf("success: %+v", s)
166 }
167 if j := groups[1].Items[0]; j.Label != "lint" || j.Href != "?job=lint&ref=main&status=success" || j.Active {
168 t.Errorf("lint: %+v", j)
169 }
170 if b := groups[2].Items[0]; b.Label != "main" || !b.Active || b.Href != "?status=success" {
171 t.Errorf("active branch clears itself: %+v", b)
172 }
173 if b := groups[2].Items[1]; b.Label != "dev" || b.Active || b.Href != "?ref=dev&status=success" {
174 t.Errorf("dev: %+v", b)
175 }
176}
internal/httpd/repohead_test.go +2 −1
@@ -25,10 +25,11 @@ func TestRepoHeaderTwoRows(t *testing.T) {
2525 Runs []buildRun
2626 Filter buildFilter
2727 FilterLinks []buildFilterLink
28 Facets []facetGroup
2829 Refs []string
2930 CanWrite bool
3031 Notice string
31 }{p, nil, nil, nil, buildFilter{}, nil, nil, true, ""})
32 }{p, nil, nil, nil, buildFilter{}, nil, nil, nil, true, ""})
3233 if err != nil {
3334 t.Fatal(err)
3435 }
internal/web/templates/builds.html +14 −7
@@ -1,19 +1,24 @@
11{{define "width"}}wide{{end}}
22{{define "title"}}builds · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
33{{define "content"}}
4<div class="listhead">
5 <h1>Builds</h1>
6 <nav class="filters">
7 {{range .FilterLinks}}<a {{if .Active}}class="active" aria-current="page" {{end}}href="{{.Href}}">{{.Label}}</a>{{end}}
8 </nav>
4<div class="withcol">
5<nav class="sidecol" aria-label="Filters">
6 {{range .Facets}}{{if .Items}}<div class="grp">
7 <h2 class="colhead">{{.Title}}</h2>
8 <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}</a></li>{{end}}</ul>
9 </div>{{end}}{{end}}
910 <form method="get" class="searchform compact">
10 <label for="ref">Branch</label>
11 <label for="ref" class="colhead">Branch</label>
1112 <input type="text" id="ref" name="ref" value="{{.Filter.Ref}}" list="buildrefs">
1213 <datalist id="buildrefs">{{range .Refs}}<option value="{{.}}">{{end}}</datalist>
1314 <input type="hidden" name="status" value="{{.Filter.Status}}">
1415 <input type="hidden" name="job" value="{{.Filter.Job}}">
1516 <button type="submit" class="btn">Filter</button>
1617 </form>
18</nav>
19<div class="colmain">
20<div class="listhead">
21 <h1>Builds</h1>
1722</div>
1823{{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}}
1924{{if and .CanWrite .Jobs}}
@@ -31,7 +36,7 @@
3136<p class="meta">Add <code>?job=name</code> for one job.</p>
3237</details>
3338<p class="meta">{{len .Runs}} run{{if ne (len .Runs) 1}}s{{end}}{{if or .Filter.Ref .Filter.Status .Filter.Job}}, <a href="?">clear filters</a>{{end}}</p>
34<ul class="loglist">
39<ul class="loglist rows">
3540{{range .Runs}}<li>
3641 <div class="commitmain">
3742 <p class="subject"><code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{printf "%.10s" .SHA}}</a></code></p>
@@ -44,4 +49,6 @@
4449</li>
4550{{else}}<li class="empty">no builds — push a commit with a <code>.gitbay/ci.yml</code></li>{{end}}
4651</ul>
52</div>
53</div>
4754{{end}}