Commit 258f283ca3
Verified · cmc
internal/web/static/style.css −1
| @@ -720,7 +720,6 @@ form.setform input[type="text"], form.setform input[type="number"], form.setform | ||
| 720 | 720 | form.setform > button, form.setform > .btngroup { justify-self: start; } |
| 721 | 721 | form.setform.stack { grid-template-columns: 1fr; } |
| 722 | 722 | form.setform.stack textarea, form.setform.stack select { width: 100%; max-width: 48rem; } |
| 723 | nav.sections { display: flex; gap: var(--sp-4); flex-wrap: wrap; font-size: var(--fs-2); margin-bottom: var(--sp-5); } | |
| 724 | 723 | ul.protlist { list-style: none; margin: var(--sp-2) 0; padding: 0; } |
| 725 | 724 | ul.protlist li { |
| 726 | 725 | display: flex; |
internal/web/templates/account.html +34 −9
| @@ -1,11 +1,26 @@ | ||
| 1 | {{define "width"}}bounded{{end}} | |
| 1 | {{define "width"}}wide{{end}} | |
| 2 | 2 | {{define "title"}}account settings{{end}} |
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <h1>Account settings</h1> |
| 5 | 5 | {{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}} |
| 6 | 6 | {{if .Message}}<p class="notice" role="status">{{.Message}}</p>{{end}} |
| 7 | 7 | |
| 8 | <h2>Profile</h2> | |
| 8 | <div class="withcol narrow"> | |
| 9 | <nav class="sidecol" aria-label="Sections"> | |
| 10 | <div class="grp"><h2 class="colhead">Sections</h2> | |
| 11 | <ul> | |
| 12 | <li><a href="#profile">Profile</a></li> | |
| 13 | <li><a href="#keys">SSH keys</a></li> | |
| 14 | <li><a href="#emails">Email addresses</a></li> | |
| 15 | <li><a href="#pgp">OpenPGP keys</a></li> | |
| 16 | <li><a href="#notifications">Notifications</a></li> | |
| 17 | <li><a href="#appearance">Appearance</a></li> | |
| 18 | <li><a href="#export">Export</a></li> | |
| 19 | <li><a href="#cli">On the command line</a></li> | |
| 20 | </ul></div> | |
| 21 | </nav> | |
| 22 | <div class="colmain"> | |
| 23 | <section id="profile"><h2>Profile</h2> | |
| 9 | 24 | <p class="meta">Shown on your profile page, <a href="/{{.Viewer}}">/{{.Viewer}}</a>.</p> |
| 10 | 25 | {{if .Draft.Is "about"}}{{template "previewblock" .Draft.HTML}}{{end}} |
| 11 | 26 | <form method="post" action="/settings" class="setform stack"> |
| @@ -21,8 +36,9 @@ | ||
| 21 | 36 | {{template "formatpicker" (.Draft.Or "about" "format" .Profile.AboutFormat)}} |
| 22 | 37 | <span class="btngroup"><button type="submit" class="btn">Save profile</button>{{template "previewbtn"}}</span> |
| 23 | 38 | </form> |
| 39 | </section> | |
| 24 | 40 | |
| 25 | <h2>SSH keys</h2> | |
| 41 | <section id="keys"><h2>SSH keys</h2> | |
| 26 | 42 | <p class="meta">Your keys are your identity here. A <code>full</code> key can run |
| 27 | 43 | commands and push; a <code>git</code> key can only move git data, which is what |
| 28 | 44 | a CI checkout wants.</p> |
| @@ -53,8 +69,9 @@ a CI checkout wants.</p> | ||
| 53 | 69 | <button type="submit" class="btn">Add key</button> |
| 54 | 70 | </form> |
| 55 | 71 | </details> |
| 72 | </section> | |
| 56 | 73 | |
| 57 | <h2>Email addresses</h2> | |
| 74 | <section id="emails"><h2>Email addresses</h2> | |
| 58 | 75 | <p class="meta">A verified address is what ties your signed commits to this |
| 59 | 76 | account, and where notifications go.</p> |
| 60 | 77 | {{if .Emails}}<ul class="plain"> |
| @@ -80,8 +97,9 @@ account, and where notifications go.</p> | ||
| 80 | 97 | <button type="submit" class="btn">Verify</button> |
| 81 | 98 | </form> |
| 82 | 99 | </details> |
| 100 | </section> | |
| 83 | 101 | |
| 84 | <h2>OpenPGP keys</h2> | |
| 102 | <section id="pgp"><h2>OpenPGP keys</h2> | |
| 85 | 103 | <p class="meta">Only needed if you sign commits with GPG. SSH signing |
| 86 | 104 | (<code>gpg.format = ssh</code>) uses the keys above.</p> |
| 87 | 105 | {{if .PGP}}<div class="tablewrap"><table class="keys"> |
| @@ -102,8 +120,9 @@ account, and where notifications go.</p> | ||
| 102 | 120 | <button type="submit" class="btn">Add key</button> |
| 103 | 121 | </form> |
| 104 | 122 | </details> |
| 123 | </section> | |
| 105 | 124 | |
| 106 | <h2>Notifications</h2> | |
| 125 | <section id="notifications"><h2>Notifications</h2> | |
| 107 | 126 | <form method="post" action="/settings" class="setform"> |
| 108 | 127 | <input type="hidden" name="field" value="notify-mail"> |
| 109 | 128 | <label for="notify-mail">Activity by mail</label> |
| @@ -118,8 +137,9 @@ account, and where notifications go.</p> | ||
| 118 | 137 | <button type="submit" class="btn">Save</button> |
| 119 | 138 | </form> |
| 120 | 139 | <p class="meta">Every issue and merge request on those repositories, as if you had watched each. A watch or mute on a repository still wins.</p> |
| 140 | </section> | |
| 121 | 141 | |
| 122 | <h2>Appearance</h2> | |
| 142 | <section id="appearance"><h2>Appearance</h2> | |
| 123 | 143 | <form method="post" action="/settings" class="setform"> |
| 124 | 144 | <input type="hidden" name="field" value="theme"> |
| 125 | 145 | <label for="theme">Colour scheme</label> |
| @@ -131,14 +151,16 @@ account, and where notifications go.</p> | ||
| 131 | 151 | <button type="submit" class="btn">Save</button> |
| 132 | 152 | </form> |
| 133 | 153 | <p class="meta">The same setting as <code>gitbay web theme set</code>. Following the browser takes its light or dark preference.</p> |
| 154 | </section> | |
| 134 | 155 | |
| 135 | <h2>Export</h2> | |
| 156 | <section id="export"><h2>Export</h2> | |
| 136 | 157 | <p class="meta">Your profile, repositories, issues and merge requests as one |
| 137 | 158 | JSON bundle, the same one <code>gitbay account export</code> writes. Keys are |
| 138 | 159 | never included; a replayed bundle's emails arrive unverified.</p> |
| 139 | 160 | <p><a href="/settings/export">Download bundle</a></p> |
| 161 | </section> | |
| 140 | 162 | |
| 141 | <h2>On the command line</h2> | |
| 163 | <section id="cli"><h2>On the command line</h2> | |
| 142 | 164 | <p class="meta">No page here yet, and nothing refusing one: a credential is |
| 143 | 165 | easier to pipe than to paste, and a minted token is shown once.</p> |
| 144 | 166 | <pre class="message">gitbay auth token create --name laptop # API tokens |
| @@ -146,4 +168,7 @@ gitbay web sessions list # browser sessions | ||
| 146 | 168 | gitbay admin ... # instance administration</pre> |
| 147 | 169 | <p class="meta">All of it works from stock OpenSSH too: |
| 148 | 170 | <code>ssh git@{{.Host}} auth whoami</code>.</p> |
| 171 | </section> | |
| 172 | </div> | |
| 173 | </div> | |
| 149 | 174 | {{end}} |
internal/web/templates/admin.html +26 −1
| @@ -1,46 +1,71 @@ | ||
| 1 | {{define "width"}}bounded{{end}} | |
| 1 | {{define "width"}}wide{{end}} | |
| 2 | 2 | {{define "title"}}admin · {{.Site}}{{end}} |
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <h1>Admin</h1> |
| 5 | 5 | <p class="meta">Server build {{.Commit}}. Every background worker's backlog, the same read as <code>gitbay dashboard --json</code> for an admin.</p> |
| 6 | 6 | <p class="meta"><a href="/admin/users">Accounts</a> — every account, with promote, demote, disable and enable.</p> |
| 7 | 7 | |
| 8 | <div class="withcol narrow"> | |
| 9 | <nav class="sidecol" aria-label="Sections"> | |
| 10 | <div class="grp"><h2 class="colhead">Sections</h2> | |
| 11 | <ul> | |
| 12 | <li><a href="#webhooks">Webhook deliveries</a></li> | |
| 13 | <li><a href="#mail">Mail</a></li> | |
| 14 | <li><a href="#mirrors">Mirrors</a></li> | |
| 15 | <li><a href="#builds">Builds</a></li> | |
| 16 | <li><a href="#deps">Dependency checks</a></li> | |
| 17 | </ul></div> | |
| 18 | </nav> | |
| 19 | <div class="colmain"> | |
| 20 | ||
| 8 | 21 | {{with .Queues.Webhooks}} |
| 22 | <section id="webhooks"> | |
| 9 | 23 | <h2>Webhook deliveries <span class="count">{{.Pending}}</span></h2> |
| 10 | 24 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} dead-lettered{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 11 | 25 | {{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> |
| 12 | 26 | {{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> |
| 13 | 27 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or dead-lettered</p>{{end}} |
| 28 | </section> | |
| 14 | 29 | {{end}} |
| 15 | 30 | |
| 16 | 31 | {{with .Queues.Mail}} |
| 32 | <section id="mail"> | |
| 17 | 33 | <h2>Mail <span class="count">{{.Pending}}</span></h2> |
| 18 | 34 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} failed{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 19 | 35 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Mail</th><th>Recipient</th><th>Subject</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody> |
| 20 | 36 | {{range .Items}}<tr><td class="mono">{{.ID}}</td><td>{{.Recipient}}</td><td>{{.Subject}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}failed {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{.LastError}}</td></tr> |
| 21 | 37 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or failed</p>{{end}} |
| 38 | </section> | |
| 22 | 39 | {{end}} |
| 23 | 40 | |
| 24 | 41 | {{with .Queues.Mirrors}} |
| 42 | <section id="mirrors"> | |
| 25 | 43 | <h2>Mirrors <span class="count">{{.Errors}}</span></h2> |
| 26 | 44 | <p class="meta">{{.Dirty}} waiting for a sync · {{.Errors}} with a failed last sync</p> |
| 27 | 45 | {{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> |
| 28 | 46 | {{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> |
| 29 | 47 | {{end}}</tbody></table></div>{{else}}<p class="none">Every mirror's last sync succeeded</p>{{end}} |
| 48 | </section> | |
| 30 | 49 | {{end}} |
| 31 | 50 | |
| 32 | 51 | {{with .Queues.Builds}} |
| 52 | <section id="builds"> | |
| 33 | 53 | <h2>Builds <span class="count">{{.Pending}}</span></h2> |
| 34 | 54 | <p class="meta">{{.Pending}} pending · {{.Running}} running{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 35 | 55 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Build</th><th>Job</th><th>Status</th><th>Since</th></tr></thead><tbody> |
| 36 | 56 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td><a href="/{{.Repo}}/builds/{{.Number}}">#{{.Number}}</a></td><td>{{.Job}}</td><td>{{.Status}}</td><td>{{if .StartedAt}}{{when .StartedAt}}{{else}}{{when .CreatedAt}}{{end}}</td></tr> |
| 37 | 57 | {{end}}</tbody></table></div>{{else}}<p class="none">No build running or pending</p>{{end}} |
| 58 | </section> | |
| 38 | 59 | {{end}} |
| 39 | 60 | |
| 40 | 61 | {{with .Queues.Deps}} |
| 62 | <section id="deps"> | |
| 41 | 63 | <h2>Dependency checks <span class="count">{{.Errors}}</span></h2> |
| 42 | 64 | {{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> |
| 43 | 65 | {{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{if .LastCheck}}{{when .LastCheck}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr> |
| 44 | 66 | {{end}}</tbody></table></div>{{else}}<p class="none">No check has failed</p>{{end}} |
| 67 | </section> | |
| 45 | 68 | {{end}} |
| 69 | </div> | |
| 70 | </div> | |
| 46 | 71 | {{end}} |
internal/web/templates/settings.html +18 −2
| @@ -1,10 +1,24 @@ | ||
| 1 | {{define "width"}}bounded{{end}} | |
| 1 | {{define "width"}}wide{{end}} | |
| 2 | 2 | {{define "title"}}settings · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 3 | 3 | {{define "content"}} |
| 4 | 4 | {{$base := printf "/%s/%s/settings" .Repo.OwnerName .Repo.Name}} |
| 5 | 5 | <h1>Settings</h1> |
| 6 | 6 | {{if .Notice}}{{if .Saved}}<p class="notice" role="status">{{.Notice}}</p>{{else}}<p class="error" role="alert">{{.Notice}}</p>{{end}}{{end}} |
| 7 | <nav class="sections" aria-label="Sections"><a href="#identity">Identity</a><a href="#access">Access</a><a href="#gates">Merge gates</a><a href="#branches">Protected branches</a><a href="#tags">Protected tags</a><a href="#deps">Dependencies</a><a href="#runners">Runners</a><a href="#lifecycle">Lifecycle</a></nav> | |
| 7 | <div class="withcol narrow"> | |
| 8 | <nav class="sidecol" aria-label="Sections"> | |
| 9 | <div class="grp"><h2 class="colhead">Sections</h2> | |
| 10 | <ul> | |
| 11 | <li><a href="#identity">Identity</a></li> | |
| 12 | <li><a href="#access">Access</a></li> | |
| 13 | <li><a href="#gates">Merge gates</a></li> | |
| 14 | <li><a href="#branches">Protected branches</a></li> | |
| 15 | <li><a href="#tags">Protected tags</a></li> | |
| 16 | <li><a href="#deps">Dependencies</a></li> | |
| 17 | <li><a href="#runners">Runners</a></li> | |
| 18 | <li><a href="#lifecycle">Lifecycle</a></li> | |
| 19 | </ul></div> | |
| 20 | </nav> | |
| 21 | <div class="colmain"> | |
| 8 | 22 | |
| 9 | 23 | <section id="identity"><h2>Identity</h2> |
| 10 | 24 | <form method="post" action="{{$base}}" class="setform"> |
| @@ -184,4 +198,6 @@ | ||
| 184 | 198 | <p class="meta">Deleting or transferring a repository is a CLI operation: |
| 185 | 199 | <code>gitbay repo delete {{.Repo.OwnerName}}/{{.Repo.Name}} --yes</code></p> |
| 186 | 200 | </section> |
| 201 | </div> | |
| 202 | </div> | |
| 187 | 203 | {{end}} |
internal/web/web_test.go +2 −2
| @@ -89,8 +89,8 @@ func TestWhenNamesTheZone(t *testing.T) { | ||
| 89 | 89 | // none. The merge request page picks wide for its diff view, so it gets |
| 90 | 90 | // a per-view define instead of a fixed one. |
| 91 | 91 | func TestMainWidthClass(t *testing.T) { |
| 92 | wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "explore.html": true, "notifications.html": true} | |
| 93 | bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "settings.html": true, "account.html": true, "admin.html": true, "adminusers.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true} | |
| 92 | wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "explore.html": true, "notifications.html": true, "settings.html": true, "account.html": true, "admin.html": true} | |
| 93 | bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "adminusers.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true} | |
| 94 | 94 | perView := map[string]string{"mr.html": `{{define "width"}}{{if eq .View "diff"}}wide{{else}}reading{{end}}{{end}}`} |
| 95 | 95 | for _, name := range Pages() { |
| 96 | 96 | src, err := TemplateSource(name) |
internal/web/widths_test.go +15
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | package web |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | "regexp" | |
| 4 | 5 | "strings" |
| 5 | 6 | "testing" |
| 6 | 7 | ) |
| @@ -33,4 +34,18 @@ func TestListPagesAreWide(t *testing.T) { | ||
| 33 | 34 | if src, _ := templateFS.ReadFile("templates/explore.html"); !strings.Contains(string(src), `<ul class="repolist rows">`) { |
| 34 | 35 | t.Error("explore.html does not use one-line rows") |
| 35 | 36 | } |
| 37 | // Settings pages carry a section column: every section id has a link | |
| 38 | // in the column, and the page is wide with the narrow grid. | |
| 39 | for _, name := range []string{"settings.html", "account.html", "admin.html"} { | |
| 40 | src, _ := templateFS.ReadFile("templates/" + name) | |
| 41 | s := string(src) | |
| 42 | if !strings.HasPrefix(s, `{{define "width"}}wide{{end}}`) || !strings.Contains(s, `<div class="withcol narrow">`) { | |
| 43 | t.Errorf("%s lacks the narrow column layout", name) | |
| 44 | } | |
| 45 | for _, m := range regexp.MustCompile(`<section id="([a-z]+)"`).FindAllStringSubmatch(s, -1) { | |
| 46 | if !strings.Contains(s, `href="#`+m[1]+`"`) { | |
| 47 | t.Errorf("%s: section %q has no link in the column", name, m[1]) | |
| 48 | } | |
| 49 | } | |
| 50 | } | |
| 36 | 51 | } |