v0.9.0 dd8597a

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

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 2

Tags 45

↑↓ moveenter openesc close