v0.28.0 fca9155

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

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