v0.10.1 e4c85c7

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.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}

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 4

Tags 45

↑↓ moveenter openesc close