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