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.Event != "" {
44 <span class="pill">{ r.Event }</span>
45 }
46 if r.Attempt > 1 {
47 <span class="pill">attempt { itoa(int(r.Attempt)) }</span>
48 }
49 </td>
50 <td class="c-sub"><a href={ templ.SafeURL(r.CHref) }>{ r.Short }</a> { r.Title }</td>
51 <td class="c-ref"><a href={ templ.SafeURL(r.RefHref) }>{ r.Ref }</a></td>
52 <td class="c-who dim">{ r.Actor }</td>
53 <td class="c-dur dim">{ r.State }{ " " }{ r.Duration }</td>
54 <td class="c-when dim">
55 if r.WhenAt != "" {
56 <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
57 }
58 </td>
59 </tr>
60}
61
62// RecentActionsBlock is the "Recent actions" section of the summary rail; its
63// id is the target of the live updates. Without a run it is hidden.
64templ RecentActionsBlock(a RecentActions) {
65 <section id="recent-actions" aria-labelledby="r-actions" hidden?={ len(a.Runs) == 0 }>
66 <h2 id="r-actions">Recent actions <a href={ templ.SafeURL(a.AllHref) }>View all</a></h2>
67 <ul class="rruns">
68 for _, r := range a.Runs {
69 <li>
70 @CheckIcon(r.Kind, r.State)
71 <a class="rr-name" href={ templ.SafeURL(r.Href) } title={ r.Workflow + " " + r.Number + " · " + r.State }>{ r.Workflow }</a>
72 <span class="mute">{ r.Number }</span>
73 <a class="rr-ref" href={ templ.SafeURL(r.RefHref) }>{ r.Ref }</a>
74 if r.WhenAt != "" {
75 <time class="dim" datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
76 }
77 </li>
78 }
79 </ul>
80 </section>
81}
82
83templ RunDetail(l Layout, p RunPage) {
84 @Shell(l, runRail(p)) {
85 @LiveSlot("run-notice")
86 <div id="run-body">
87 @RunBody(p)
88 </div>
89 @RunSummaries(p.Summaries, p.SummariesMore)
90 }
91}
92
93// runBodyFragment is the body of the run page as a live fragment.
94templ runBodyFragment(p RunPage) {
95 <div id="run-body">
96 @RunBody(p)
97 </div>
98}
99
100// RunBody is the jobs of a run, replaced by the live updates.
101templ RunBody(p RunPage) {
102 <h2 class="run-h">@CheckIcon(p.Kind, p.State)<span>{ p.Workflow }</span> <span class="mute">{ p.Number }</span></h2>
103 if p.Error != "" {
104 <pre class="run-err">{ p.Error }</pre>
105 }
106 if len(p.Annotations) > 0 {
107 <section class="annots" aria-labelledby="run-annots">
108 <h3 id="run-annots">Annotations <span class="count">{ itoa(len(p.Annotations)) }</span></h3>
109 @AnnotationList(p.Annotations, p.AnnotationsMore, false)
110 </section>
111 }
112 <ul class="jobs">
113 for _, j := range p.Jobs {
114 <li id={ "job-" + j.ID }>
115 <a class="job-h" href={ templ.SafeURL(j.Href) }>
116 @CheckIcon(j.Kind, j.State)
117 <span class="j-name">{ j.Name }</span>
118 <span class="dim">{ j.State }{ " " }{ j.Duration }</span>
119 </a>
120 if j.Runner != "" {
121 <span class="mute">on { runnerText(j) }</span>
122 }
123 if len(j.Needs) > 0 {
124 <span class="mute">needs { joinWords(j.Needs) }</span>
125 }
126 if j.Error != "" {
127 <p class="job-err">{ j.Error }</p>
128 }
129 if len(j.Steps) > 0 {
130 <ol class="steps">
131 for _, s := range j.Steps {
132 <li>@CheckIcon(s.Kind, s.State) <span>{ s.Name }</span> <span class="dim">{ s.Duration }</span></li>
133 }
134 </ol>
135 }
136 </li>
137 }
138 </ul>
139}
140
141// AnnotationList is a list of annotations: the level as a mark with its word,
142// the job and the step, the title and the message, and file:line. Everything
143// comes from the workflow and is text. Without grouped, each item names its job (the
144// run page); grouped lists the annotations by step instead (the job page).
145templ AnnotationList(anns []AnnotationView, more bool, grouped bool) {
146 <ul class="annot-list">
147 for _, a := range anns {
148 <li class={ "annot", "annot-" + a.Kind }>
149 if grouped && a.StepHead {
150 <p class="annot-step">
151 if a.StepHref != "" {
152 <a href={ templ.SafeURL(a.StepHref) }>{ a.Step }</a>
153 } else {
154 { a.Step }
155 }
156 </p>
157 }
158 <span class={ "an", "an-" + a.Kind } role="img" aria-label={ a.Level } title={ a.Level }></span>
159 <div class="annot-body">
160 <p class="annot-head">
161 <span class="annot-level">{ a.Level }</span>
162 if !grouped {
163 if a.JobHref != "" {
164 <a href={ templ.SafeURL(a.JobHref) }>{ a.Job }</a>
165 } else {
166 <span>{ a.Job }</span>
167 }
168 <span class="mute">{ a.Step }</span>
169 }
170 if a.Title != "" {
171 <strong>{ a.Title }</strong>
172 }
173 </p>
174 <p class="annot-msg">{ a.Message }</p>
175 if a.Where != "" {
176 <p class="annot-where">
177 if a.WhereHref != "" {
178 <a href={ templ.SafeURL(a.WhereHref) }>{ a.Where }</a>
179 } else {
180 <span>{ a.Where }</span>
181 }
182 </p>
183 }
184 </div>
185 </li>
186 }
187 </ul>
188 if more {
189 <p class="mute">more annotations exist than are shown.</p>
190 }
191}
192
193// JobAnnotations is the annotations of the job page, replaced by the live
194// updates; the element exists even when there is none, so that the first one
195// has something to replace.
196templ JobAnnotations(anns []AnnotationView, more bool) {
197 <section id="job-annotations" class="annots" aria-label="Annotations">
198 if len(anns) > 0 {
199 <h3>Annotations <span class="count">{ itoa(len(anns)) }</span></h3>
200 @AnnotationList(anns, more, true)
201 }
202 </section>
203}
204
205// RunSummaries is the job summaries of a run, one block per step that wrote
206// one. The Markdown is rendered by the README renderer: raw HTML dropped, URLs
207// filtered, images shown as their alt text.
208templ RunSummaries(sums []JobSummaries, more bool) {
209 for _, j := range sums {
210 <section class="summaries" aria-label={ "Summary of " + j.Job }>
211 <h3>
212 if j.JobHref != "" {
213 <a href={ templ.SafeURL(j.JobHref) }>{ j.Job }</a>
214 } else {
215 { j.Job }
216 }
217 <span class="mute">summary</span>
218 </h3>
219 for _, s := range j.Steps {
220 <article class="prose summary" aria-label={ s.Step } hx-ignore>
221 <h4 class="summary-step">{ s.Step }</h4>
222 if s.Note != "" {
223 <p class="dim">{ s.Note }</p>
224 } else {
225 @templ.Raw(s.HTML)
226 }
227 </article>
228 }
229 if j.More {
230 <p class="mute">the job has more summaries than the first 20, which are shown.</p>
231 }
232 </section>
233 }
234 if more {
235 <p class="mute">more summaries are not shown: the summaries of the page are over { humanSize(maxSummaryPageBytes) }.</p>
236 }
237}
238
239templ runRail(p RunPage) {
240 <section aria-labelledby="r-run">
241 <h2 id="r-run">Run</h2>
242 <dl class="kv">
243 <dt>result</dt>
244 <dd>{ p.State }</dd>
245 <dt>event</dt>
246 <dd>{ p.Event }</dd>
247 <dt>commit</dt>
248 <dd><a href={ templ.SafeURL(p.CHref) }>{ p.Short }</a></dd>
249 <dt>ref</dt>
250 <dd><a href={ templ.SafeURL(p.RefHref) }>{ p.Ref }</a></dd>
251 <dt>actor</dt>
252 <dd>{ p.Actor }</dd>
253 <dt>attempt</dt>
254 <dd>{ itoa(int(p.Attempt)) } of { itoa(int(p.Latest)) }</dd>
255 </dl>
256 if len(p.Attempts) > 0 {
257 <ul class="attempts">
258 for _, a := range p.Attempts {
259 <li>
260 if a.Current {
261 <span aria-current="page">{ a.Label }</span>
262 } else {
263 <a href={ templ.SafeURL(a.Href) }>{ a.Label }</a>
264 }
265 </li>
266 }
267 </ul>
268 }
269 </section>
270 if p.CancelAction != "" || p.RerunAction != "" {
271 <section aria-labelledby="r-act" class="decide">
272 <h2 id="r-act">Actions</h2>
273 <div class="d-btns">
274 if p.CancelAction != "" {
275 <form method="post" action={ templ.SafeURL(p.CancelAction) }><button type="submit" class="btn">Cancel run</button></form>
276 }
277 if p.RerunAction != "" {
278 <form method="post" action={ templ.SafeURL(p.RerunAction) }><button type="submit" class="btn primary">Re-run all jobs</button></form>
279 }
280 if p.CanRerunFailed {
281 <form method="post" action={ templ.SafeURL(p.RerunFailedAction) }><button type="submit" class="btn">Re-run failed jobs</button></form>
282 }
283 </div>
284 </section>
285 }
286}
287
288// JobHead is the title of the job page with its state and its error, replaced
289// by the live updates. total is the number of lines the log holds.
290templ JobHead(j JobRow, total int64) {
291 <div id="job-head" data-total={ itoa64(total) }>
292 <h2 class="run-h">@CheckIcon(j.Kind, j.State)<span>{ j.Name }</span> <span class="dim">{ j.State }{ " " }{ j.Duration }</span></h2>
293 if j.Error != "" {
294 <p class="job-err">{ j.Error }</p>
295 }
296 </div>
297}
298
299// JobSteps is the steps of the job in the rail, replaced by the live updates.
300templ JobSteps(steps []StepView) {
301 <ol class="steps" id="job-steps">
302 for _, s := range steps {
303 <li>
304 @CheckIcon(s.Kind, s.State)
305 if s.LogEnd > s.LogStart {
306 <a href={ templ.SafeURL("#L" + itoa64(s.LogStart)) }>{ s.Name }</a>
307 } else {
308 <span>{ s.Name }</span>
309 }
310 <span class="dim">{ s.Duration }</span>
311 </li>
312 }
313 </ol>
314}
315
316templ Job(l Layout, p JobPage) {
317 @Shell(l, jobRail(p)) {
318 @LiveSlot("job-notice")
319 @JobHead(p.JobRow, p.Total)
320 @JobAnnotations(p.Annotations, p.AnnotationsMore)
321 <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))) }>
322 if p.Note != "" {
323 <p class="dim">{ p.Note }</p>
324 }
325 if p.Prev != "" {
326 <p class="more"><a href={ templ.SafeURL(p.Prev) } rel="prev">earlier lines</a></p>
327 }
328 <ol class="log" start={ itoa64(p.From + 1) }>
329 for _, ln := range p.Lines {
330 @LogLine(ln)
331 }
332 </ol>
333 if p.Next != "" {
334 <p class="more"><a href={ templ.SafeURL(p.Next) } rel="next">more lines</a></p>
335 }
336 </div>
337 }
338}
339
340// LogLine is a line of the log. Group and error markers of the runner show as
341// headings and errors, never as raw text.
342templ LogLine(ln LogLineView) {
343 <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>
344}
345
346templ jobRail(p JobPage) {
347 <section aria-labelledby="r-job">
348 <h2 id="r-job">Steps</h2>
349 @JobSteps(p.Steps)
350 </section>
351 <section aria-labelledby="r-jobf">
352 <h2 id="r-jobf">Job</h2>
353 <dl class="kv">
354 <dt>run</dt>
355 <dd><a href={ templ.SafeURL(p.RunHref) }>{ p.RunName }</a></dd>
356 if p.Runner != "" {
357 <dt>runner</dt>
358 <dd>{ p.Runner }</dd>
359 }
360 if p.Isolation != "" {
361 <dt>isolation</dt>
362 <dd>{ p.Isolation }</dd>
363 }
364 <dt>lines</dt>
365 <dd>{ itoa64(p.Total) }</dd>
366 </dl>
367 <p><a href={ templ.SafeURL(p.RawHref) }>raw log</a></p>
368 </section>
369}
370
371// CommitChecks is the combined state of the checks of a commit, as a mark
372// with a count, linking to the page that explains it.
373templ CommitChecks(c ChecksSummary) {
374 if c.Total > 0 {
375 <a class="ck-sum" href={ templ.SafeURL(c.Href) } title={ c.Title }>
376 @CheckIcon(c.Kind, c.State)
377 <span class="mute">{ c.Label }</span>
378 </a>
379 }
380}
381
382// MRChecks is the checks section of the rail of a merge request.
383templ MRChecks(c ChecksSection) {
384 <section aria-labelledby="r-checks" id="mr-checks">
385 <h2 id="r-checks">Checks <span class="count">{ itoa(len(c.Rows)) }</span></h2>
386 if len(c.Rows) == 0 {
387 <p class="dim">no checks</p>
388 } else {
389 <ul class="checks">
390 for _, r := range c.Rows {
391 <li>
392 @CheckIcon(r.Kind, r.State)
393 if r.Href != "" {
394 <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
395 } else {
396 <span>{ r.Name }</span>
397 }
398 <span class="dim">{ r.State }</span>
399 if r.Waiting {
400 <span class="mute">waiting for a runner</span>
401 }
402 </li>
403 }
404 </ul>
405 }
406 if c.Note != "" {
407 <p class="mute">{ c.Note }</p>
408 }
409 </section>
410}