Commit 19682e7e50
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
internal/web/templates/admin.html +5 −5
| @@ -6,7 +6,7 @@ | |||
| 6 | {{with .Queues.Webhooks}} | 6 | {{with .Queues.Webhooks}} |
| 7 | <h2>Webhook deliveries <span class="count">{{.Pending}}</span></h2> | 7 | <h2>Webhook deliveries <span class="count">{{.Pending}}</span></h2> |
| 8 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} dead-lettered{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> | 8 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} dead-lettered{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 9 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr><th>Repository</th><th>URL</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody> | 9 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>URL</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody> |
| 10 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{.URL}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}dead-lettered {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{if .Status}}{{.Status}} {{end}}{{.LastError}}</td></tr> | 10 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{.URL}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}dead-lettered {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{if .Status}}{{.Status}} {{end}}{{.LastError}}</td></tr> |
| 11 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or dead-lettered</p>{{end}} | 11 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or dead-lettered</p>{{end}} |
| 12 | {{end}} | 12 | {{end}} |
| @@ -14,7 +14,7 @@ | |||
| 14 | {{with .Queues.Mail}} | 14 | {{with .Queues.Mail}} |
| 15 | <h2>Mail <span class="count">{{.Pending}}</span></h2> | 15 | <h2>Mail <span class="count">{{.Pending}}</span></h2> |
| 16 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} failed{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> | 16 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} failed{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 17 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr><th>Recipient</th><th>Subject</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody> | 17 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Recipient</th><th>Subject</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody> |
| 18 | {{range .Items}}<tr><td>{{.Recipient}}</td><td>{{.Subject}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}failed {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{.LastError}}</td></tr> | 18 | {{range .Items}}<tr><td>{{.Recipient}}</td><td>{{.Subject}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}failed {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{.LastError}}</td></tr> |
| 19 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or failed</p>{{end}} | 19 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or failed</p>{{end}} |
| 20 | {{end}} | 20 | {{end}} |
| @@ -22,7 +22,7 @@ | |||
| 22 | {{with .Queues.Mirrors}} | 22 | {{with .Queues.Mirrors}} |
| 23 | <h2>Mirrors <span class="count">{{.Errors}}</span></h2> | 23 | <h2>Mirrors <span class="count">{{.Errors}}</span></h2> |
| 24 | <p class="meta">{{.Dirty}} waiting for a sync · {{.Errors}} with a failed last sync</p> | 24 | <p class="meta">{{.Dirty}} waiting for a sync · {{.Errors}} with a failed last sync</p> |
| 25 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr><th>Repository</th><th>Direction</th><th>URL</th><th>Last sync</th><th>Error</th></tr></thead><tbody> | 25 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Direction</th><th>URL</th><th>Last sync</th><th>Error</th></tr></thead><tbody> |
| 26 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{.Direction}}</td><td>{{.URL}}</td><td>{{if .LastSync}}{{when .LastSync}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr> | 26 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{.Direction}}</td><td>{{.URL}}</td><td>{{if .LastSync}}{{when .LastSync}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr> |
| 27 | {{end}}</tbody></table></div>{{else}}<p class="none">Every mirror's last sync succeeded</p>{{end}} | 27 | {{end}}</tbody></table></div>{{else}}<p class="none">Every mirror's last sync succeeded</p>{{end}} |
| 28 | {{end}} | 28 | {{end}} |
| @@ -30,14 +30,14 @@ | |||
| 30 | {{with .Queues.Builds}} | 30 | {{with .Queues.Builds}} |
| 31 | <h2>Builds <span class="count">{{.Pending}}</span></h2> | 31 | <h2>Builds <span class="count">{{.Pending}}</span></h2> |
| 32 | <p class="meta">{{.Pending}} pending · {{.Running}} running{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> | 32 | <p class="meta">{{.Pending}} pending · {{.Running}} running{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 33 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr><th>Repository</th><th>Build</th><th>Job</th><th>Running since</th></tr></thead><tbody> | 33 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Build</th><th>Job</th><th>Running since</th></tr></thead><tbody> |
| 34 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td><a href="/{{.Repo}}/builds/{{.Number}}">#{{.Number}}</a></td><td>{{.Job}}</td><td>{{when .StartedAt}}</td></tr> | 34 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td><a href="/{{.Repo}}/builds/{{.Number}}">#{{.Number}}</a></td><td>{{.Job}}</td><td>{{when .StartedAt}}</td></tr> |
| 35 | {{end}}</tbody></table></div>{{else}}<p class="none">No build running</p>{{end}} | 35 | {{end}}</tbody></table></div>{{else}}<p class="none">No build running</p>{{end}} |
| 36 | {{end}} | 36 | {{end}} |
| 37 | 37 | ||
| 38 | {{with .Queues.Deps}} | 38 | {{with .Queues.Deps}} |
| 39 | <h2>Dependency checks <span class="count">{{.Errors}}</span></h2> | 39 | <h2>Dependency checks <span class="count">{{.Errors}}</span></h2> |
| 40 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr><th>Repository</th><th>Last check</th><th>Error</th></tr></thead><tbody> | 40 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Last check</th><th>Error</th></tr></thead><tbody> |
| 41 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{if .LastCheck}}{{when .LastCheck}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr> | 41 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{if .LastCheck}}{{when .LastCheck}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr> |
| 42 | {{end}}</tbody></table></div>{{else}}<p class="none">No check has failed</p>{{end}} | 42 | {{end}}</tbody></table></div>{{else}}<p class="none">No check has failed</p>{{end}} |
| 43 | {{end}} | 43 | {{end}} |
internal/web/web_test.go +55 −1
| @@ -1,6 +1,11 @@ | |||
| 1 | package web | 1 | package web |
| 2 | 2 | ||
| 3 | import "testing" | 3 | import ( |
| 4 | "io/fs" | ||
| 5 | "regexp" | ||
| 6 | "strings" | ||
| 7 | "testing" | ||
| 8 | ) | ||
| 4 | 9 | ||
| 5 | // Every page template parses with the layout at start-up, so a broken | 10 | // Every page template parses with the layout at start-up, so a broken |
| 6 | // template fails the process rather than the first visit to its page. | 11 | // template fails the process rather than the first visit to its page. |
| @@ -17,3 +22,52 @@ func TestEveryPageTemplateParses(t *testing.T) { | |||
| 17 | } | 22 | } |
| 18 | } | 23 | } |
| 19 | } | 24 | } |
| 25 | |||
| 26 | // A th with no rule of its own falls back to the browser's centred default | ||
| 27 | // (#151). The classes that do left-align one are read out of the stylesheet | ||
| 28 | // rather than listed here, so a new rule does not need a test change. | ||
| 29 | func TestHeaderRowsAreLeftAligned(t *testing.T) { | ||
| 30 | aligned := map[string]bool{} | ||
| 31 | for _, rule := range strings.Split(string(StyleCSS), "}") { | ||
| 32 | sel, decls, ok := strings.Cut(rule, "{") | ||
| 33 | if !ok || !strings.Contains(decls, "text-align: left") || !strings.Contains(sel, "th") { | ||
| 34 | continue | ||
| 35 | } | ||
| 36 | for _, class := range regexp.MustCompile(`\.([a-z-]+)`).FindAllStringSubmatch(sel, -1) { | ||
| 37 | aligned[class[1]] = true | ||
| 38 | } | ||
| 39 | } | ||
| 40 | if len(aligned) == 0 { | ||
| 41 | t.Fatal("no left-aligning th rule found in the stylesheet") | ||
| 42 | } | ||
| 43 | |||
| 44 | classes := regexp.MustCompile(`class="([^"]*)"`) | ||
| 45 | entries, err := fs.ReadDir(templateFS, "templates") | ||
| 46 | if err != nil { | ||
| 47 | t.Fatal(err) | ||
| 48 | } | ||
| 49 | for _, e := range entries { | ||
| 50 | b, err := templateFS.ReadFile("templates/" + e.Name()) | ||
| 51 | if err != nil { | ||
| 52 | t.Fatal(err) | ||
| 53 | } | ||
| 54 | // The table and row tags a header sits under, plus the header row | ||
| 55 | // itself: any one of them may carry the class. | ||
| 56 | for _, head := range strings.Split(string(b), "<thead>")[1:] { | ||
| 57 | open, _, _ := strings.Cut(head, "</tr>") | ||
| 58 | before := strings.Split(string(b), "<thead>"+head)[0] | ||
| 59 | if i := strings.LastIndex(before, "<table"); i >= 0 { | ||
| 60 | open += before[i:] | ||
| 61 | } | ||
| 62 | hit := false | ||
| 63 | for _, m := range classes.FindAllStringSubmatch(open, -1) { | ||
| 64 | for _, c := range strings.Fields(m[1]) { | ||
| 65 | hit = hit || aligned[c] | ||
| 66 | } | ||
| 67 | } | ||
| 68 | if !hit { | ||
| 69 | t.Errorf("%s: header row is not left-aligned by any rule: %.60s", e.Name(), open) | ||
| 70 | } | ||
| 71 | } | ||
| 72 | } | ||
| 73 | } | ||