v0.25.1 7fabed9

actions.templ RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close