1package web
2
3// The CI pages: the runs of a repository, a run with its jobs, a job with its
4// steps and its log (actions.go). A state is always a word as well as a
5// symbol and a colour.
6
7// CheckIcon is the symbol of a state: a mark in the colour of its kind, with
8// the state as its text for a screen reader.
9templ CheckIcon(kind, state string) {
10 <span class={ "ck", "ck-" + kind } role="img" aria-label={ state } title={ state }></span>
11}
12
13templ Actions(l Layout, p ActionsPage) {
14 @Shell(l, nil) {
15 if len(p.Runs) == 0 {
16 <p class="dim">no runs yet: a push that matches a workflow of .github/workflows starts one.</p>
17 } else {
18 <table class="list runs" aria-label="Runs">
19 <thead>
20 <tr><th class="c-st"></th><th class="c-name">workflow</th><th class="c-sub">commit</th><th class="c-ref">ref</th><th class="c-who">actor</th><th class="c-dur">time</th><th class="c-when">created</th></tr>
21 </thead>
22 <tbody id="run-rows" data-rows>
23 for _, r := range p.Runs {
24 @RunListRow(r)
25 }
26 </tbody>
27 </table>
28 }
29 if p.More != "" {
30 <p class="more"><a href={ templ.SafeURL(p.More) } rel="next">older runs</a></p>
31 }
32 }
33}
34
35// RunListRow is a row of the list of runs; its id is the target of the live
36// updates.
37templ RunListRow(r RunRow) {
38 <tr id={ "run-" + r.ID }>
39 <td class="c-st">@CheckIcon(r.Kind, r.State)</td>
40 <td class="c-name">
41 <a href={ templ.SafeURL(r.Href) }>{ r.Workflow }</a>
42 <span class="mute">{ r.Number }</span>
43 if r.Attempt > 1 {
44 <span class="pill">attempt { itoa(int(r.Attempt)) }</span>
45 }
46 </td>
47 <td class="c-sub"><a href={ templ.SafeURL(r.CHref) }>{ r.Short }</a> { r.Title }</td>
48 <td class="c-ref"><a href={ templ.SafeURL(r.RefHref) }>{ r.Ref }</a></td>
49 <td class="c-who dim">{ r.Actor }</td>
50 <td class="c-dur dim">{ r.State }{ " " }{ r.Duration }</td>
51 <td class="c-when dim">
52 if r.WhenAt != "" {
53 <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
54 }
55 </td>
56 </tr>
57}
58
59templ RunDetail(l Layout, p RunPage) {
60 @Shell(l, runRail(p)) {
61 @LiveSlot("run-notice")
62 <div id="run-body">
63 @RunBody(p)
64 </div>
65 }
66}
67
68// runBodyFragment is the body of the run page as a live fragment.
69templ runBodyFragment(p RunPage) {
70 <div id="run-body">
71 @RunBody(p)
72 </div>
73}
74
75// RunBody is the jobs of a run, replaced by the live updates.
76templ RunBody(p RunPage) {
77 <h2 class="run-h">@CheckIcon(p.Kind, p.State)<span>{ p.Workflow }</span> <span class="mute">{ p.Number }</span></h2>
78 if p.Error != "" {
79 <pre class="run-err">{ p.Error }</pre>
80 }
81 <ul class="jobs">
82 for _, j := range p.Jobs {
83 <li id={ "job-" + j.ID }>
84 <a class="job-h" href={ templ.SafeURL(j.Href) }>
85 @CheckIcon(j.Kind, j.State)
86 <span class="j-name">{ j.Name }</span>
87 <span class="dim">{ j.State }{ " " }{ j.Duration }</span>
88 </a>
89 if j.Runner != "" {
90 <span class="mute">on { runnerText(j) }</span>
91 }
92 if len(j.Needs) > 0 {
93 <span class="mute">needs { joinWords(j.Needs) }</span>
94 }
95 if j.Error != "" {
96 <p class="job-err">{ j.Error }</p>
97 }
98 if len(j.Steps) > 0 {
99 <ol class="steps">
100 for _, s := range j.Steps {
101 <li>@CheckIcon(s.Kind, s.State) <span>{ s.Name }</span> <span class="dim">{ s.Duration }</span></li>
102 }
103 </ol>
104 }
105 </li>
106 }
107 </ul>
108}
109
110templ runRail(p RunPage) {
111 <section aria-labelledby="r-run">
112 <h2 id="r-run">Run</h2>
113 <dl class="kv">
114 <dt>result</dt>
115 <dd>{ p.State }</dd>
116 <dt>commit</dt>
117 <dd><a href={ templ.SafeURL(p.CHref) }>{ p.Short }</a></dd>
118 <dt>ref</dt>
119 <dd><a href={ templ.SafeURL(p.RefHref) }>{ p.Ref }</a></dd>
120 <dt>actor</dt>
121 <dd>{ p.Actor }</dd>
122 <dt>attempt</dt>
123 <dd>{ itoa(int(p.Attempt)) } of { itoa(int(p.Latest)) }</dd>
124 </dl>
125 if len(p.Attempts) > 0 {
126 <ul class="attempts">
127 for _, a := range p.Attempts {
128 <li>
129 if a.Current {
130 <span aria-current="page">{ a.Label }</span>
131 } else {
132 <a href={ templ.SafeURL(a.Href) }>{ a.Label }</a>
133 }
134 </li>
135 }
136 </ul>
137 }
138 </section>
139 if p.CancelAction != "" || p.RerunAction != "" {
140 <section aria-labelledby="r-act" class="decide">
141 <h2 id="r-act">Actions</h2>
142 <div class="d-btns">
143 if p.CancelAction != "" {
144 <form method="post" action={ templ.SafeURL(p.CancelAction) }><button type="submit" class="btn">Cancel run</button></form>
145 }
146 if p.RerunAction != "" {
147 <form method="post" action={ templ.SafeURL(p.RerunAction) }><button type="submit" class="btn primary">Re-run all jobs</button></form>
148 }
149 if p.CanRerunFailed {
150 <form method="post" action={ templ.SafeURL(p.RerunFailedAction) }><button type="submit" class="btn">Re-run failed jobs</button></form>
151 }
152 </div>
153 </section>
154 }
155}
156
157// JobHead is the title of the job page with its state and its error, replaced
158// by the live updates. total is the number of lines the log holds.
159templ JobHead(j JobRow, total int64) {
160 <div id="job-head" data-total={ itoa64(total) }>
161 <h2 class="run-h">@CheckIcon(j.Kind, j.State)<span>{ j.Name }</span> <span class="dim">{ j.State }{ " " }{ j.Duration }</span></h2>
162 if j.Error != "" {
163 <p class="job-err">{ j.Error }</p>
164 }
165 </div>
166}
167
168// JobSteps is the steps of the job in the rail, replaced by the live updates.
169templ JobSteps(steps []StepView) {
170 <ol class="steps" id="job-steps">
171 for _, s := range steps {
172 <li>
173 @CheckIcon(s.Kind, s.State)
174 if s.LogEnd > s.LogStart {
175 <a href={ templ.SafeURL("#L" + itoa64(s.LogStart)) }>{ s.Name }</a>
176 } else {
177 <span>{ s.Name }</span>
178 }
179 <span class="dim">{ s.Duration }</span>
180 </li>
181 }
182 </ol>
183}
184
185templ Job(l Layout, p JobPage) {
186 @Shell(l, jobRail(p)) {
187 @LiveSlot("job-notice")
188 @JobHead(p.JobRow, p.Total)
189 <div id="job-log" class="joblog" data-total={ itoa64(p.Total) } data-follow?={ p.Follow } data-stream={ p.StreamHref } data-next={ itoa64(p.From + int64(len(p.Lines))) }>
190 if p.Note != "" {
191 <p class="dim">{ p.Note }</p>
192 }
193 if p.Prev != "" {
194 <p class="more"><a href={ templ.SafeURL(p.Prev) } rel="prev">earlier lines</a></p>
195 }
196 <ol class="log" start={ itoa64(p.From + 1) }>
197 for _, ln := range p.Lines {
198 @LogLine(ln)
199 }
200 </ol>
201 if p.Next != "" {
202 <p class="more"><a href={ templ.SafeURL(p.Next) } rel="next">more lines</a></p>
203 }
204 </div>
205 }
206}
207
208// LogLine is a line of the log. Group and error markers of the runner show as
209// headings and errors, never as raw text.
210templ LogLine(ln LogLineView) {
211 <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) } data-cont?={ ln.Continued }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li>
212}
213
214templ jobRail(p JobPage) {
215 <section aria-labelledby="r-job">
216 <h2 id="r-job">Steps</h2>
217 @JobSteps(p.Steps)
218 </section>
219 <section aria-labelledby="r-jobf">
220 <h2 id="r-jobf">Job</h2>
221 <dl class="kv">
222 <dt>run</dt>
223 <dd><a href={ templ.SafeURL(p.RunHref) }>{ p.RunName }</a></dd>
224 if p.Runner != "" {
225 <dt>runner</dt>
226 <dd>{ p.Runner }</dd>
227 }
228 if p.Isolation != "" {
229 <dt>isolation</dt>
230 <dd>{ p.Isolation }</dd>
231 }
232 <dt>lines</dt>
233 <dd>{ itoa64(p.Total) }</dd>
234 </dl>
235 <p><a href={ templ.SafeURL(p.RawHref) }>raw log</a></p>
236 </section>
237}
238
239// CommitChecks is the combined state of the checks of a commit, as a mark
240// with a count, linking to the page that explains it.
241templ CommitChecks(c ChecksSummary) {
242 if c.Total > 0 {
243 <a class="ck-sum" href={ templ.SafeURL(c.Href) } title={ c.Title }>
244 @CheckIcon(c.Kind, c.State)
245 <span class="mute">{ c.Label }</span>
246 </a>
247 }
248}
249
250// MRChecks is the checks section of the rail of a merge request.
251templ MRChecks(c ChecksSection) {
252 <section aria-labelledby="r-checks" id="mr-checks">
253 <h2 id="r-checks">Checks <span class="count">{ itoa(len(c.Rows)) }</span></h2>
254 if len(c.Rows) == 0 {
255 <p class="dim">no checks</p>
256 } else {
257 <ul class="checks">
258 for _, r := range c.Rows {
259 <li>
260 @CheckIcon(r.Kind, r.State)
261 if r.Href != "" {
262 <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
263 } else {
264 <span>{ r.Name }</span>
265 }
266 <span class="dim">{ r.State }</span>
267 if r.Waiting {
268 <span class="mute">waiting for a runner</span>
269 }
270 </li>
271 }
272 </ul>
273 }
274 if c.Note != "" {
275 <p class="mute">{ c.Note }</p>
276 }
277 </section>
278}