feat(ci): add the Actions pages, live job logs and check marks
/<repo>/actions lists the runs, /<repo>/actions/runs/<id> shows a run with its jobs and steps (attempt switcher, cancel and re-run for the owner), and the job page shows the steps and the log with folded groups, a raw log link and a live feed of the lines over Server-Sent Events. The repository event stream gets the views actions, run and job: they carry states only, never log lines. The commit page, the commit and refs lists and the merge request page show the checks of their commit, and the merge request rail says when a job waits for a runner. The web reads and writes through ports.RunService only.
- commit
- 1769e4f987d0ec226c85f70fe6f2e07a0d129f39
- author
- brice · · 2026-10-12 18:39 UTC
- parent
- 1bd4fe7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
54 files · +4219 -261
A internal/adapters/web/actions.go +824 -0
| @@ -0,0 +1,824 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "fmt" | |
| 9 | + | "io" | |
| 10 | + | "net/http" | |
| 11 | + | "strconv" | |
| 12 | + | "strings" | |
| 13 | + | "time" | |
| 14 | + | ||
| 15 | + | "github.com/a-h/templ" | |
| 16 | + | "github.com/bnema/zerowrap" | |
| 17 | + | ||
| 18 | + | "bnema.dev/ermite/internal/clientlimit" | |
| 19 | + | "bnema.dev/ermite/internal/domain" | |
| 20 | + | "bnema.dev/ermite/internal/ports" | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | // The CI pages (ADR 0025): the runs of a repository, one run with its jobs, | |
| 24 | + | // one job with its steps and its log. They read through ports.RunService and | |
| 25 | + | // follow the events RunUpdated and JobUpdated of the repository topic. | |
| 26 | + | // | |
| 27 | + | // GET /<repo>/actions the runs, newest first | |
| 28 | + | // GET /<repo>/actions/runs/<id> one run: jobs and steps | |
| 29 | + | // GET /<repo>/actions/runs/<id>/job/<job id> one job: steps and live log | |
| 30 | + | // GET /<repo>/actions/runs/<id>/job/<job id>/raw the log as text | |
| 31 | + | // POST /<repo>/actions/runs/<id>/cancel | rerun | rerun-failed (the owner) | |
| 32 | + | ||
| 33 | + | const ( | |
| 34 | + | // actionsPageSize is the number of runs one page lists. | |
| 35 | + | actionsPageSize = 30 | |
| 36 | + | // jobLogPageLines is the number of log lines a job page shows at once; the | |
| 37 | + | // rest is reached with ?from=. | |
| 38 | + | jobLogPageLines = 2000 | |
| 39 | + | // rawLogMaxBytes bounds the raw log the server writes to one response. | |
| 40 | + | rawLogMaxBytes = 64 << 20 | |
| 41 | + | ) | |
| 42 | + | ||
| 43 | + | func actionsBase(repo string) string { return repoHref(repo, "actions") } | |
| 44 | + | func runHref(repo string, id int64) string { | |
| 45 | + | return repoHref(repo, "actions", "runs", strconv.FormatInt(id, 10)) | |
| 46 | + | } | |
| 47 | + | func jobHref(repo string, runID int64, jobID string) string { | |
| 48 | + | return runHref(repo, runID) + "/job/" + jobID | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // ciState is the one word shown for a status and a conclusion: what is going | |
| 52 | + | // on, or how it ended. | |
| 53 | + | func ciState(status domain.CIStatus, c domain.CIConclusion) string { | |
| 54 | + | switch status { | |
| 55 | + | case domain.CIQueued: | |
| 56 | + | return "queued" | |
| 57 | + | case domain.CIInProgress: | |
| 58 | + | return "running" | |
| 59 | + | } | |
| 60 | + | switch c { | |
| 61 | + | case domain.CISuccess: | |
| 62 | + | return "success" | |
| 63 | + | case domain.CIFailure: | |
| 64 | + | return "failure" | |
| 65 | + | case domain.CICancelled: | |
| 66 | + | return "cancelled" | |
| 67 | + | case domain.CISkipped: | |
| 68 | + | return "skipped" | |
| 69 | + | case domain.CITimedOut: | |
| 70 | + | return "timed out" | |
| 71 | + | case domain.CINeutral: | |
| 72 | + | return "neutral" | |
| 73 | + | case domain.CIActionRequired: | |
| 74 | + | return "action required" | |
| 75 | + | } | |
| 76 | + | return "done" | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // ciKind groups the states into the few that have an icon and a colour. | |
| 80 | + | func ciKind(status domain.CIStatus, c domain.CIConclusion) string { | |
| 81 | + | switch { | |
| 82 | + | case status == domain.CIQueued: | |
| 83 | + | return "queued" | |
| 84 | + | case status == domain.CIInProgress: | |
| 85 | + | return "running" | |
| 86 | + | case c == domain.CISuccess, c == domain.CINeutral: | |
| 87 | + | return "ok" | |
| 88 | + | case c == domain.CISkipped: | |
| 89 | + | return "skipped" | |
| 90 | + | case c == domain.CICancelled: | |
| 91 | + | return "cancelled" | |
| 92 | + | } | |
| 93 | + | return "bad" | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // duration is a short elapsed time between two instants; empty when the | |
| 97 | + | // start is unknown, running up to now when the end is not. | |
| 98 | + | func duration(start, end, now time.Time) string { | |
| 99 | + | if start.IsZero() { | |
| 100 | + | return "" | |
| 101 | + | } | |
| 102 | + | if end.IsZero() { | |
| 103 | + | end = now | |
| 104 | + | } | |
| 105 | + | d := end.Sub(start).Round(time.Second) | |
| 106 | + | switch { | |
| 107 | + | case d < 0: | |
| 108 | + | return "" | |
| 109 | + | case d < time.Minute: | |
| 110 | + | return fmt.Sprintf("%ds", int(d.Seconds())) | |
| 111 | + | case d < time.Hour: | |
| 112 | + | return fmt.Sprintf("%dm %02ds", int(d.Minutes()), int(d.Seconds())%60) | |
| 113 | + | } | |
| 114 | + | return fmt.Sprintf("%dh %02dm", int(d.Hours()), int(d.Minutes())%60) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // RunRow is a run of the list. | |
| 118 | + | type RunRow struct { | |
| 119 | + | ID string | |
| 120 | + | Href string | |
| 121 | + | Kind string // ciKind | |
| 122 | + | State string // ciState | |
| 123 | + | Workflow string | |
| 124 | + | Title string | |
| 125 | + | Ref string | |
| 126 | + | RefHref string | |
| 127 | + | Short string | |
| 128 | + | CHref string | |
| 129 | + | Actor string | |
| 130 | + | When string | |
| 131 | + | WhenAt string | |
| 132 | + | Duration string | |
| 133 | + | Number string | |
| 134 | + | Attempt int32 | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // ActionsPage lists the runs of a repository. | |
| 138 | + | type ActionsPage struct { | |
| 139 | + | Runs []RunRow | |
| 140 | + | // More is the link to the next page, "" on the last. | |
| 141 | + | More string | |
| 142 | + | // NoRunner says that CI has no runner to run on yet. | |
| 143 | + | NoRunner bool | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // StepView is a step of a job. | |
| 147 | + | type StepView struct { | |
| 148 | + | Number int32 | |
| 149 | + | Name string | |
| 150 | + | Kind, State string | |
| 151 | + | Duration string | |
| 152 | + | // LogHref jumps to the lines of the step in the job log. | |
| 153 | + | LogStart, LogEnd int64 | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // JobRow is a job of a run. | |
| 157 | + | type JobRow struct { | |
| 158 | + | ID, Name, Href string | |
| 159 | + | Kind, State string | |
| 160 | + | Runner string | |
| 161 | + | Isolation string | |
| 162 | + | Duration string | |
| 163 | + | Error string | |
| 164 | + | Needs []string | |
| 165 | + | Steps []StepView | |
| 166 | + | Attempt int32 | |
| 167 | + | // Waiting says that the job is queued for a runner that no registered | |
| 168 | + | // runner can be. | |
| 169 | + | Waiting bool | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // RunPage is one run with its jobs. | |
| 173 | + | type RunPage struct { | |
| 174 | + | RunRow | |
| 175 | + | Sha, Error string | |
| 176 | + | Attempt int32 | |
| 177 | + | Latest int32 | |
| 178 | + | Attempts []AttemptLink | |
| 179 | + | Jobs []JobRow | |
| 180 | + | Active bool | |
| 181 | + | // Forms are the owner's actions; nil/empty when the visitor cannot. | |
| 182 | + | CancelAction, RerunAction, RerunFailedAction string | |
| 183 | + | CanRerunFailed bool | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // AttemptLink switches between the attempts of a run. | |
| 187 | + | type AttemptLink struct { | |
| 188 | + | Label, Href string | |
| 189 | + | Current bool | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | // LogLineView is a line of a job log, as shown. | |
| 193 | + | type LogLineView struct { | |
| 194 | + | N int64 | |
| 195 | + | Step int32 | |
| 196 | + | Text string | |
| 197 | + | Time string | |
| 198 | + | // Group marks "##[group]" and "##[endgroup]" lines and Error the | |
| 199 | + | // "##[error]" ones; the text is shown without the marker. | |
| 200 | + | Group, End, Error bool | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // JobPage is one job with its steps and its log. | |
| 204 | + | type JobPage struct { | |
| 205 | + | JobRow | |
| 206 | + | RunHref string | |
| 207 | + | RunName string | |
| 208 | + | Run string | |
| 209 | + | Steps []StepView | |
| 210 | + | Lines []LogLineView | |
| 211 | + | // From is the first line shown; Next is the link to the lines after the | |
| 212 | + | // last shown, "" when it is the end; Follow says the log is still being | |
| 213 | + | // written, so the page follows it live. | |
| 214 | + | From int64 | |
| 215 | + | Next string | |
| 216 | + | Prev string | |
| 217 | + | Total int64 | |
| 218 | + | Follow bool | |
| 219 | + | RawHref string | |
| 220 | + | // StreamHref is the live feed of the lines (logStream). | |
| 221 | + | StreamHref string | |
| 222 | + | Note string | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // --- handlers --- | |
| 226 | + | ||
| 227 | + | // actions is GET /<repo>/actions. | |
| 228 | + | func (s *Server) actions(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 229 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 230 | + | if err != nil { | |
| 231 | + | return nil, 0, err | |
| 232 | + | } | |
| 233 | + | token := r.URL.Query().Get("page") | |
| 234 | + | page, err := s.cfg.Runs.ListRuns(ctx, principal(ctx), d.repo.Name, ports.ListRunsParams{PageSize: actionsPageSize, PageToken: token}) | |
| 235 | + | if err != nil { | |
| 236 | + | return nil, 0, err | |
| 237 | + | } | |
| 238 | + | now := s.cfg.Now() | |
| 239 | + | p := ActionsPage{} | |
| 240 | + | for _, run := range page.Runs { | |
| 241 | + | p.Runs = append(p.Runs, s.runRow(d.name(), run, now)) | |
| 242 | + | } | |
| 243 | + | if page.NextPageToken != "" { | |
| 244 | + | p.More = actionsBase(d.name()) + "?page=" + page.NextPageToken | |
| 245 | + | } | |
| 246 | + | l := s.repoLayout(ctx, d, "actions") | |
| 247 | + | l.Title = "actions · " + d.name() + " · " + s.instance | |
| 248 | + | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=actions") | |
| 249 | + | l.Crumbs[0].Current = false | |
| 250 | + | l.Crumbs = append(l.Crumbs, Crumb{Label: "actions", Current: true}) | |
| 251 | + | return Actions(l, p), http.StatusOK, nil | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (s *Server) runRow(repo string, run domain.CIRun, now time.Time) RunRow { | |
| 255 | + | row := RunRow{ | |
| 256 | + | ID: strconv.FormatInt(run.ID, 10), Href: runHref(repo, run.ID), | |
| 257 | + | Kind: ciKind(run.Status, run.Conclusion), State: ciState(run.Status, run.Conclusion), | |
| 258 | + | Workflow: visibleText(run.WorkflowName), Title: visibleText(run.Title), | |
| 259 | + | Ref: visibleText(strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/")), | |
| 260 | + | Short: short(run.Sha), CHref: repoHref(repo, "commit", run.Sha), Actor: visibleText(run.Actor), | |
| 261 | + | Duration: duration(run.StartedAt, run.CompletedAt, now), Number: "#" + strconv.FormatInt(run.Number, 10), Attempt: run.Attempt, | |
| 262 | + | } | |
| 263 | + | row.RefHref = repoHref(repo, "tree", strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/")) | |
| 264 | + | row.When, row.WhenAt = whenOf(now, run.CreatedAt) | |
| 265 | + | return row | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | func stepViews(j ports.JobView, now time.Time) []StepView { | |
| 269 | + | ranges := j.StepRanges() | |
| 270 | + | out := make([]StepView, 0, len(j.Steps)) | |
| 271 | + | for i, st := range j.Steps { | |
| 272 | + | status := domain.CIQueued | |
| 273 | + | switch st.State { | |
| 274 | + | case domain.CIStepInProgress: | |
| 275 | + | status = domain.CIInProgress | |
| 276 | + | case domain.CIStepCompleted: | |
| 277 | + | status = domain.CICompleted | |
| 278 | + | } | |
| 279 | + | out = append(out, StepView{ | |
| 280 | + | Number: st.Number, Name: visibleText(st.Name), Kind: ciKind(status, st.Conclusion), State: ciState(status, st.Conclusion), | |
| 281 | + | Duration: duration(st.StartedAt, st.CompletedAt, now), LogStart: ranges[i].Start, LogEnd: ranges[i].End, | |
| 282 | + | }) | |
| 283 | + | } | |
| 284 | + | return out | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | func (s *Server) jobRow(repo string, j ports.JobView, now time.Time) JobRow { | |
| 288 | + | return JobRow{ | |
| 289 | + | ID: j.ID, Name: visibleText(j.Name), Href: jobHref(repo, j.RunID, j.ID), Kind: ciKind(j.Status(), j.Conclusion), | |
| 290 | + | State: ciState(j.Status(), j.Conclusion), Runner: visibleText(j.RunnerName), Isolation: string(j.Isolation), | |
| 291 | + | Duration: duration(j.StartedAt, j.CompletedAt, now), Error: visibleText(j.Error), Needs: j.Needs, | |
| 292 | + | Steps: stepViews(j, now), Attempt: j.Attempt, | |
| 293 | + | } | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | func parseRunPath(r *http.Request) (int64, error) { | |
| 297 | + | id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) | |
| 298 | + | if err != nil || id < 1 { | |
| 299 | + | return 0, errNotFound | |
| 300 | + | } | |
| 301 | + | return id, nil | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | // run is GET /<repo>/actions/runs/<id>. | |
| 305 | + | func (s *Server) run(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 306 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 307 | + | if err != nil { | |
| 308 | + | return nil, 0, err | |
| 309 | + | } | |
| 310 | + | id, err := parseRunPath(r) | |
| 311 | + | if err != nil { | |
| 312 | + | return nil, 0, err | |
| 313 | + | } | |
| 314 | + | var attempt int32 | |
| 315 | + | if a := r.URL.Query().Get("attempt"); a != "" { | |
| 316 | + | n, perr := strconv.ParseInt(a, 10, 32) | |
| 317 | + | if perr != nil || n < 1 { | |
| 318 | + | return nil, 0, errNotFound | |
| 319 | + | } | |
| 320 | + | attempt = int32(n) | |
| 321 | + | } | |
| 322 | + | p, err := s.runPage(ctx, d, id, attempt) | |
| 323 | + | if err != nil { | |
| 324 | + | return nil, 0, err | |
| 325 | + | } | |
| 326 | + | l := s.repoLayout(ctx, d, "actions") | |
| 327 | + | l.Title = "run " + p.ID + " · " + d.name() + " · " + s.instance | |
| 328 | + | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=run&n="+p.ID) | |
| 329 | + | l.Crumbs[0].Current = false | |
| 330 | + | l.Crumbs = append(l.Crumbs, Crumb{Label: "actions", Href: actionsBase(d.name())}, Crumb{Label: p.Workflow + " " + p.Number, Current: true}) | |
| 331 | + | l.HasRail = true | |
| 332 | + | return RunDetail(l, p), http.StatusOK, nil | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | //hexcheck:ignore suspicious-business-logic-in-adapter builds the run page from the use case results; no rule is decided here | |
| 336 | + | func (s *Server) runPage(ctx context.Context, d *repoData, id int64, attempt int32) (RunPage, error) { | |
| 337 | + | detail, err := s.cfg.Runs.GetRun(ctx, principal(ctx), d.repo.Name, id, attempt) | |
| 338 | + | if err != nil { | |
| 339 | + | return RunPage{}, err | |
| 340 | + | } | |
| 341 | + | now := s.cfg.Now() | |
| 342 | + | run := detail.Run | |
| 343 | + | p := RunPage{ | |
| 344 | + | RunRow: s.runRow(d.name(), run, now), Sha: run.Sha, Error: visibleText(run.Error), | |
| 345 | + | Attempt: run.Attempt, Latest: detail.LatestAttempt, Active: run.Status != domain.CICompleted, | |
| 346 | + | } | |
| 347 | + | for a := detail.LatestAttempt; a >= 1 && detail.LatestAttempt > 1; a-- { | |
| 348 | + | p.Attempts = append(p.Attempts, AttemptLink{ | |
| 349 | + | Label: "attempt " + strconv.Itoa(int(a)), Href: runHref(d.name(), id) + "?attempt=" + strconv.Itoa(int(a)), Current: a == run.Attempt, | |
| 350 | + | }) | |
| 351 | + | } | |
| 352 | + | for _, j := range detail.Jobs { | |
| 353 | + | row := s.jobRow(d.name(), j, now) | |
| 354 | + | if run.Attempt == detail.LatestAttempt { | |
| 355 | + | // An earlier attempt keeps the job as it ran then. | |
| 356 | + | row.Href = jobHref(d.name(), id, j.ID) | |
| 357 | + | } | |
| 358 | + | p.Jobs = append(p.Jobs, row) | |
| 359 | + | } | |
| 360 | + | if SignedIn(ctx) && run.Attempt == detail.LatestAttempt { | |
| 361 | + | base := runHref(d.name(), id) | |
| 362 | + | if p.Active { | |
| 363 | + | p.CancelAction = base + "/cancel" | |
| 364 | + | } else if len(detail.Jobs) > 0 { | |
| 365 | + | p.RerunAction = base + "/rerun" | |
| 366 | + | for _, j := range detail.Jobs { | |
| 367 | + | if j.Conclusion.IsFailure() || j.Conclusion == domain.CICancelled { | |
| 368 | + | p.RerunFailedAction, p.CanRerunFailed = base+"/rerun-failed", true | |
| 369 | + | break | |
| 370 | + | } | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | } | |
| 374 | + | return p, nil | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | // job is GET /<repo>/actions/runs/<id>/job/<job id>. | |
| 378 | + | func (s *Server) job(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 379 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 380 | + | if err != nil { | |
| 381 | + | return nil, 0, err | |
| 382 | + | } | |
| 383 | + | id, err := parseRunPath(r) | |
| 384 | + | if err != nil { | |
| 385 | + | return nil, 0, err | |
| 386 | + | } | |
| 387 | + | jobID := r.PathValue("job") | |
| 388 | + | if runOf, _, _, ok := domain.ParseJobID(jobID); !ok || runOf != id { | |
| 389 | + | return nil, 0, errNotFound | |
| 390 | + | } | |
| 391 | + | var attempt int32 | |
| 392 | + | if a := r.URL.Query().Get("attempt"); a != "" { | |
| 393 | + | n, perr := strconv.ParseInt(a, 10, 32) | |
| 394 | + | if perr != nil || n < 1 { | |
| 395 | + | return nil, 0, errNotFound | |
| 396 | + | } | |
| 397 | + | attempt = int32(n) | |
| 398 | + | } | |
| 399 | + | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt) | |
| 400 | + | if err != nil { | |
| 401 | + | return nil, 0, err | |
| 402 | + | } | |
| 403 | + | run, err := s.cfg.Runs.GetRun(ctx, principal(ctx), d.repo.Name, id, 0) | |
| 404 | + | if err != nil { | |
| 405 | + | return nil, 0, err | |
| 406 | + | } | |
| 407 | + | now := s.cfg.Now() | |
| 408 | + | var from int64 | |
| 409 | + | if f := r.URL.Query().Get("from"); f != "" { | |
| 410 | + | from, err = strconv.ParseInt(f, 10, 64) | |
| 411 | + | if err != nil || from < 0 { | |
| 412 | + | return nil, 0, errNotFound | |
| 413 | + | } | |
| 414 | + | } | |
| 415 | + | p := JobPage{ | |
| 416 | + | JobRow: s.jobRow(d.name(), view, now), RunHref: runHref(d.name(), id), RunName: visibleText(run.Run.WorkflowName), | |
| 417 | + | Run: strconv.FormatInt(id, 10), Steps: stepViews(view, now), From: from, Total: view.Lines, | |
| 418 | + | RawHref: jobHref(d.name(), id, jobID) + "/raw", | |
| 419 | + | } | |
| 420 | + | if attempt > 0 { | |
| 421 | + | p.RawHref += "?attempt=" + strconv.Itoa(int(attempt)) | |
| 422 | + | } | |
| 423 | + | p.Follow = !view.Ended() | |
| 424 | + | p.StreamHref = jobHref(d.name(), id, jobID) + "/stream" | |
| 425 | + | lines, next, err := s.readLog(ctx, d.repo.Name, view, from, jobLogPageLines) | |
| 426 | + | if err != nil { | |
| 427 | + | return nil, 0, err | |
| 428 | + | } | |
| 429 | + | p.Lines = lines | |
| 430 | + | if next < view.Lines { | |
| 431 | + | p.Next = jobHref(d.name(), id, jobID) + "?from=" + strconv.FormatInt(next, 10) | |
| 432 | + | p.Follow = false // more is waiting before the live end | |
| 433 | + | } | |
| 434 | + | if from > 0 { | |
| 435 | + | p.Prev = jobHref(d.name(), id, jobID) + "?from=" + strconv.FormatInt(max(from-jobLogPageLines, 0), 10) | |
| 436 | + | } | |
| 437 | + | if view.Phase == domain.CIPhaseWaiting || view.Phase == domain.CIPhaseReady { | |
| 438 | + | p.Note = "the job has not started: no log yet." | |
| 439 | + | } else if view.Lines == 0 && view.Ended() { | |
| 440 | + | p.Note = "the job wrote no log." | |
| 441 | + | } | |
| 442 | + | l := s.repoLayout(ctx, d, "actions") | |
| 443 | + | l.Title = view.Name + " · run " + p.Run + " · " + d.name() + " · " + s.instance | |
| 444 | + | l.Live = true | |
| 445 | + | l.LiveURL = liveURL("repo:"+d.name(), "view=job&job="+jobID) | |
| 446 | + | l.Crumbs[0].Current = false | |
| 447 | + | l.Crumbs = append(l.Crumbs, | |
| 448 | + | Crumb{Label: "actions", Href: actionsBase(d.name())}, | |
| 449 | + | Crumb{Label: p.RunName + " #" + strconv.FormatInt(run.Run.Number, 10), Href: p.RunHref}, | |
| 450 | + | Crumb{Label: p.Name, Current: true}) | |
| 451 | + | l.HasRail = true | |
| 452 | + | return Job(l, p), http.StatusOK, nil | |
| 453 | + | } | |
| 454 | + | ||
| 455 | + | // readLog reads up to max lines from a line number, through the log stream of | |
| 456 | + | // the use case, and returns the line after the last read. | |
| 457 | + | func (s *Server) readLog(ctx context.Context, repo domain.RepoName, j ports.JobView, from int64, max int) ([]LogLineView, int64, error) { | |
| 458 | + | if from > j.Lines || j.Lines == 0 { | |
| 459 | + | return nil, from, nil | |
| 460 | + | } | |
| 461 | + | var out []LogLineView | |
| 462 | + | next := from | |
| 463 | + | ranges := j.StepRanges() | |
| 464 | + | stepOf := func(n int64) int32 { | |
| 465 | + | for i, r := range ranges { | |
| 466 | + | if n >= r.Start && n < r.End { | |
| 467 | + | return j.Steps[i].Number | |
| 468 | + | } | |
| 469 | + | } | |
| 470 | + | return 0 | |
| 471 | + | } | |
| 472 | + | stop := errors.New("enough") | |
| 473 | + | err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from}, func(b ports.LogBatch) error { | |
| 474 | + | for i, l := range b.Lines { | |
| 475 | + | if len(out) >= max { | |
| 476 | + | return stop | |
| 477 | + | } | |
| 478 | + | n := b.Offset + int64(i) | |
| 479 | + | text := l.Text | |
| 480 | + | v := LogLineView{N: n, Step: stepOf(n), Time: l.Time.UTC().Format("15:04:05")} | |
| 481 | + | switch { | |
| 482 | + | case strings.HasPrefix(text, "##[group]"): | |
| 483 | + | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |
| 484 | + | case strings.HasPrefix(text, "##[endgroup]"): | |
| 485 | + | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |
| 486 | + | case strings.HasPrefix(text, "##[error]"): | |
| 487 | + | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |
| 488 | + | } | |
| 489 | + | if l.Continued && len(out) > 0 { | |
| 490 | + | out[len(out)-1].Text += visibleText(text) | |
| 491 | + | next = n + 1 | |
| 492 | + | continue | |
| 493 | + | } | |
| 494 | + | v.Text = visibleText(text) | |
| 495 | + | out = append(out, v) | |
| 496 | + | next = n + 1 | |
| 497 | + | } | |
| 498 | + | return nil | |
| 499 | + | }) | |
| 500 | + | if err != nil && !errors.Is(err, stop) && !errors.Is(err, domain.ErrNotFound) { | |
| 501 | + | return nil, from, err | |
| 502 | + | } | |
| 503 | + | return out, next, nil | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | // rawLog is GET /<repo>/actions/runs/<id>/job/<job id>/raw: the log as text, | |
| 507 | + | // one line per line with its time, joined where the runner cut a long line. | |
| 508 | + | func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) { | |
| 509 | + | ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) | |
| 510 | + | defer cancel() | |
| 511 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 512 | + | if err != nil { | |
| 513 | + | s.fail(ctx, w, r, err) | |
| 514 | + | return | |
| 515 | + | } | |
| 516 | + | id, err := parseRunPath(r) | |
| 517 | + | jobID := r.PathValue("job") | |
| 518 | + | if runOf, _, _, ok := domain.ParseJobID(jobID); err != nil || !ok || runOf != id { | |
| 519 | + | s.fail(ctx, w, r, errNotFound) | |
| 520 | + | return | |
| 521 | + | } | |
| 522 | + | var attempt int32 | |
| 523 | + | if a := r.URL.Query().Get("attempt"); a != "" { | |
| 524 | + | n, perr := strconv.ParseInt(a, 10, 32) | |
| 525 | + | if perr != nil || n < 1 { | |
| 526 | + | s.fail(ctx, w, r, errNotFound) | |
| 527 | + | return | |
| 528 | + | } | |
| 529 | + | attempt = int32(n) | |
| 530 | + | } | |
| 531 | + | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt) | |
| 532 | + | if err != nil { | |
| 533 | + | s.fail(ctx, w, r, err) | |
| 534 | + | return | |
| 535 | + | } | |
| 536 | + | h := w.Header() | |
| 537 | + | h.Set("Content-Type", "text/plain; charset=utf-8") | |
| 538 | + | h.Set("X-Content-Type-Options", "nosniff") | |
| 539 | + | h.Set("Content-Security-Policy", "default-src 'none'; sandbox") | |
| 540 | + | h.Set("Content-Disposition", fmt.Sprintf(`inline; filename="job-%s.log"`, jobID)) | |
| 541 | + | cacheHeaders(ctx, h, "no-cache", sessionCache) | |
| 542 | + | written := 0 | |
| 543 | + | open := false | |
| 544 | + | err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{JobID: jobID, Attempt: attempt}, func(b ports.LogBatch) error { | |
| 545 | + | for _, l := range b.Lines { | |
| 546 | + | if written > rawLogMaxBytes { | |
| 547 | + | return errors.New("log too large") | |
| 548 | + | } | |
| 549 | + | var line string | |
| 550 | + | switch { | |
| 551 | + | case l.Continued && open: | |
| 552 | + | line = l.Text | |
| 553 | + | case open: | |
| 554 | + | line = "\n" + l.Time.UTC().Format(time.RFC3339Nano) + " " + l.Text | |
| 555 | + | default: | |
| 556 | + | line = l.Time.UTC().Format(time.RFC3339Nano) + " " + l.Text | |
| 557 | + | } | |
| 558 | + | open = true | |
| 559 | + | n, werr := fmt.Fprint(w, line) | |
| 560 | + | written += n | |
| 561 | + | if werr != nil { | |
| 562 | + | return werr | |
| 563 | + | } | |
| 564 | + | } | |
| 565 | + | return nil | |
| 566 | + | }) | |
| 567 | + | if open { | |
| 568 | + | _, _ = fmt.Fprintln(w) | |
| 569 | + | } | |
| 570 | + | if err != nil && !open && errors.Is(err, domain.ErrNotFound) { | |
| 571 | + | _ = view | |
| 572 | + | w.WriteHeader(http.StatusNotFound) | |
| 573 | + | } | |
| 574 | + | } | |
| 575 | + | ||
| 576 | + | // --- the owner's actions --- | |
| 577 | + | ||
| 578 | + | // ciAction wraps a POST of the actions pages: the form gate, the bounded | |
| 579 | + | // context and the mapping of a refusal to a page. | |
| 580 | + | func (s *Server) ciAction(h func(ctx context.Context, r *http.Request) (to string, err error)) http.HandlerFunc { | |
| 581 | + | return s.mrAction(func(ctx context.Context, r *http.Request) (string, string, error) { | |
| 582 | + | to, err := h(ctx, r) | |
| 583 | + | return to, actionsBase(r.PathValue("repo")), err | |
| 584 | + | }) | |
| 585 | + | } | |
| 586 | + | ||
| 587 | + | func (s *Server) runCancel(ctx context.Context, r *http.Request) (string, error) { | |
| 588 | + | name, id, err := s.runTarget(r) | |
| 589 | + | if err != nil { | |
| 590 | + | return "", err | |
| 591 | + | } | |
| 592 | + | if _, err := s.cfg.Runs.CancelRun(ctx, principal(ctx), name, id); err != nil { | |
| 593 | + | return "", err | |
| 594 | + | } | |
| 595 | + | return runHref(name.String(), id), nil | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | func (s *Server) runRerun(failedOnly bool) func(ctx context.Context, r *http.Request) (string, error) { | |
| 599 | + | return func(ctx context.Context, r *http.Request) (string, error) { | |
| 600 | + | name, id, err := s.runTarget(r) | |
| 601 | + | if err != nil { | |
| 602 | + | return "", err | |
| 603 | + | } | |
| 604 | + | if _, err := s.cfg.Runs.RerunRun(ctx, principal(ctx), name, id, failedOnly); err != nil { | |
| 605 | + | return "", err | |
| 606 | + | } | |
| 607 | + | return runHref(name.String(), id), nil | |
| 608 | + | } | |
| 609 | + | } | |
| 610 | + | ||
| 611 | + | func (s *Server) runTarget(r *http.Request) (domain.RepoName, int64, error) { | |
| 612 | + | name, err := domain.ParseRepoName(r.PathValue("repo")) | |
| 613 | + | if err != nil { | |
| 614 | + | return domain.RepoName{}, 0, errNotFound | |
| 615 | + | } | |
| 616 | + | id, err := parseRunPath(r) | |
| 617 | + | return name, id, err | |
| 618 | + | } | |
| 619 | + | ||
| 620 | + | func itoa64(n int64) string { return strconv.FormatInt(n, 10) } | |
| 621 | + | ||
| 622 | + | // runnerText names the runner of a job and how it isolated it. | |
| 623 | + | func runnerText(j JobRow) string { | |
| 624 | + | if j.Isolation == "" { | |
| 625 | + | return j.Runner | |
| 626 | + | } | |
| 627 | + | return j.Runner + " (" + j.Isolation + ")" | |
| 628 | + | } | |
| 629 | + | ||
| 630 | + | func joinWords(ws []string) string { return strings.Join(ws, ", ") } | |
| 631 | + | ||
| 632 | + | // logClass is the class of a line of the log. | |
| 633 | + | func logClass(ln LogLineView) string { | |
| 634 | + | switch { | |
| 635 | + | case ln.Group: | |
| 636 | + | return "lg group" | |
| 637 | + | case ln.End: | |
| 638 | + | return "lg endgroup" | |
| 639 | + | case ln.Error: | |
| 640 | + | return "lg error" | |
| 641 | + | } | |
| 642 | + | return "lg" | |
| 643 | + | } | |
| 644 | + | ||
| 645 | + | // ChecksSummary is the combined state of the checks of a commit. | |
| 646 | + | type ChecksSummary struct { | |
| 647 | + | Total int | |
| 648 | + | Kind, State string | |
| 649 | + | Label, Title string | |
| 650 | + | Href string | |
| 651 | + | } | |
| 652 | + | ||
| 653 | + | // CheckRow is a check of a commit or a merge request. | |
| 654 | + | type CheckRow struct { | |
| 655 | + | Name, Kind, State, Href string | |
| 656 | + | Waiting bool | |
| 657 | + | } | |
| 658 | + | ||
| 659 | + | // ChecksSection is the checks of the head of a merge request. | |
| 660 | + | type ChecksSection struct { | |
| 661 | + | Rows []CheckRow | |
| 662 | + | // Note explains the rule that applies to them. | |
| 663 | + | Note string | |
| 664 | + | } | |
| 665 | + | ||
| 666 | + | // checksFor reads the checks of a commit as the caller of the request. A page | |
| 667 | + | // shows them when it can: a failure of the read is logged and the page goes on | |
| 668 | + | // without, since the code and the review do not depend on CI. | |
| 669 | + | func (s *Server) checksFor(ctx context.Context, repo domain.RepoName, sha string) []ports.CheckView { | |
| 670 | + | if s.cfg.Runs == nil || sha == "" { | |
| 671 | + | return nil | |
| 672 | + | } | |
| 673 | + | res, err := s.cfg.Runs.ListChecks(ctx, principal(ctx), repo, sha) | |
| 674 | + | if err != nil { | |
| 675 | + | if !errors.Is(err, domain.ErrNotFound) { | |
| 676 | + | log := zerowrap.FromCtx(ctx) | |
| 677 | + | log.Warn().Err(err).Str("repo", repo.String()).Msg("read the checks of a commit") | |
| 678 | + | } | |
| 679 | + | return nil | |
| 680 | + | } | |
| 681 | + | return res.Checks | |
| 682 | + | } | |
| 683 | + | ||
| 684 | + | // checksSummary reduces checks to the mark a list shows next to a commit. | |
| 685 | + | func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) ChecksSummary { | |
| 686 | + | if len(checks) == 0 { | |
| 687 | + | return ChecksSummary{} | |
| 688 | + | } | |
| 689 | + | plain := make([]domain.CICheck, 0, len(checks)) | |
| 690 | + | passed := 0 | |
| 691 | + | for _, c := range checks { | |
| 692 | + | plain = append(plain, c.CICheck) | |
| 693 | + | if c.Status == domain.CICompleted && c.Conclusion.PassesMerge() { | |
| 694 | + | passed++ | |
| 695 | + | } | |
| 696 | + | } | |
| 697 | + | sum := ChecksSummary{Total: len(checks), Href: repoHref(repo.String(), "actions"), Label: fmt.Sprintf("%d/%d", passed, len(checks))} | |
| 698 | + | switch domain.CombineChecks(plain) { | |
| 699 | + | case domain.CheckStateSuccess: | |
| 700 | + | sum.Kind, sum.State = "ok", "checks passed" | |
| 701 | + | case domain.CheckStateFailure: | |
| 702 | + | sum.Kind, sum.State = "bad", "checks failed" | |
| 703 | + | default: | |
| 704 | + | sum.Kind, sum.State = "running", "checks pending" | |
| 705 | + | } | |
| 706 | + | sum.Title = sum.State + ": " + sum.Label | |
| 707 | + | return sum | |
| 708 | + | } | |
| 709 | + | ||
| 710 | + | // checksSection is the checks of the rail of a merge request, with the rule of | |
| 711 | + | // the repository that applies to them. | |
| 712 | + | func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) ChecksSection { | |
| 713 | + | sec := ChecksSection{} | |
| 714 | + | for _, c := range checks { | |
| 715 | + | row := CheckRow{ | |
| 716 | + | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | |
| 717 | + | Waiting: c.WaitingForRunner, | |
| 718 | + | } | |
| 719 | + | if c.JobID != "" { | |
| 720 | + | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | |
| 721 | + | } else { | |
| 722 | + | row.Href = runHref(repo.Name.String(), c.RunID) | |
| 723 | + | } | |
| 724 | + | sec.Rows = append(sec.Rows, row) | |
| 725 | + | } | |
| 726 | + | switch { | |
| 727 | + | case len(checks) == 0: | |
| 728 | + | sec.Note = "no check on this commit: nothing blocks the merge." | |
| 729 | + | case repo.Settings.FailingChecksBlock: | |
| 730 | + | sec.Note = "a failing or unfinished check blocks the merge." | |
| 731 | + | default: | |
| 732 | + | sec.Note = "checks do not block the merge in this repository." | |
| 733 | + | } | |
| 734 | + | _ = now | |
| 735 | + | return sec | |
| 736 | + | } | |
| 737 | + | ||
| 738 | + | // logStream is GET /<repo>/actions/runs/<id>/job/<job id>/stream?from=N: the | |
| 739 | + | // lines of a job log from line N as Server-Sent Events, following the job while | |
| 740 | + | // it runs. Each "ci-log" event holds the <li> of the lines of one batch and has | |
| 741 | + | // the number of the next line as its id, so that a reconnection (Last-Event-ID) | |
| 742 | + | // goes on where it stopped. "ci-end" tells the job is over and every line was | |
| 743 | + | // sent. The job events of the repository stream carry no line (see events.go). | |
| 744 | + | func (s *Server) logStream(w http.ResponseWriter, r *http.Request) { | |
| 745 | + | ctx, cancel := context.WithCancel(r.Context()) | |
| 746 | + | defer cancel() | |
| 747 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 748 | + | if err != nil { | |
| 749 | + | s.denyEvents(ctx, w, err) | |
| 750 | + | return | |
| 751 | + | } | |
| 752 | + | id, err := parseRunPath(r) | |
| 753 | + | jobID := r.PathValue("job") | |
| 754 | + | if runOf, _, _, ok := domain.ParseJobID(jobID); err != nil || !ok || runOf != id { | |
| 755 | + | s.denyEvents(ctx, w, errNotFound) | |
| 756 | + | return | |
| 757 | + | } | |
| 758 | + | from := int64(0) | |
| 759 | + | if v := r.Header.Get("Last-Event-ID"); v != "" { | |
| 760 | + | from, err = strconv.ParseInt(v, 10, 64) | |
| 761 | + | } else if v := r.URL.Query().Get("from"); v != "" { | |
| 762 | + | from, err = strconv.ParseInt(v, 10, 64) | |
| 763 | + | } | |
| 764 | + | if err != nil || from < 0 { | |
| 765 | + | s.denyEvents(ctx, w, errNotFound) | |
| 766 | + | return | |
| 767 | + | } | |
| 768 | + | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0) | |
| 769 | + | if err != nil { | |
| 770 | + | s.denyEvents(ctx, w, err) | |
| 771 | + | return | |
| 772 | + | } | |
| 773 | + | ||
| 774 | + | rc := http.NewResponseController(w) | |
| 775 | + | hdr := w.Header() | |
| 776 | + | hdr.Set("Content-Type", "text/event-stream") | |
| 777 | + | cacheHeaders(ctx, hdr, "no-cache, no-transform", sessionCache+", no-transform") | |
| 778 | + | hdr.Set("X-Accel-Buffering", "no") | |
| 779 | + | w.WriteHeader(http.StatusOK) | |
| 780 | + | out := &sseWriter{w: w, rc: rc} | |
| 781 | + | if out.raw(": connected\n\n") != nil { | |
| 782 | + | return | |
| 783 | + | } | |
| 784 | + | ranges := view.StepRanges() | |
| 785 | + | _ = ranges | |
| 786 | + | n := from | |
| 787 | + | err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{ | |
| 788 | + | JobID: jobID, Offset: from, Follow: true, | |
| 789 | + | Client: clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies)), | |
| 790 | + | }, func(b ports.LogBatch) error { | |
| 791 | + | var html strings.Builder | |
| 792 | + | for i, l := range b.Lines { | |
| 793 | + | v := LogLineView{N: b.Offset + int64(i), Time: l.Time.UTC().Format("15:04:05"), Step: l.Step} | |
| 794 | + | text := l.Text | |
| 795 | + | switch { | |
| 796 | + | case strings.HasPrefix(text, "##[group]"): | |
| 797 | + | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |
| 798 | + | case strings.HasPrefix(text, "##[endgroup]"): | |
| 799 | + | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |
| 800 | + | case strings.HasPrefix(text, "##[error]"): | |
| 801 | + | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |
| 802 | + | } | |
| 803 | + | v.Text = visibleText(text) | |
| 804 | + | if l.Continued { | |
| 805 | + | continue // joined to its line by the page reload; rare and harmless | |
| 806 | + | } | |
| 807 | + | var b2 strings.Builder | |
| 808 | + | if err := LogLine(v).Render(ctx, &b2); err != nil { | |
| 809 | + | return err | |
| 810 | + | } | |
| 811 | + | html.WriteString(strings.ReplaceAll(b2.String(), "\n", "")) | |
| 812 | + | } | |
| 813 | + | n = b.Offset + int64(len(b.Lines)) | |
| 814 | + | _ = rc.SetWriteDeadline(time.Now().Add(writeWindow)) | |
| 815 | + | _, werr := io.WriteString(w, "event: ci-log\nid: "+strconv.FormatInt(n, 10)+"\ndata: "+html.String()+"\n\n") | |
| 816 | + | if werr != nil { | |
| 817 | + | return werr | |
| 818 | + | } | |
| 819 | + | return rc.Flush() | |
| 820 | + | }) | |
| 821 | + | if err == nil { | |
| 822 | + | _ = out.raw("event: ci-end\nid: " + strconv.FormatInt(n, 10) + "\ndata: end\n\n") | |
| 823 | + | } | |
| 824 | + | } | |
A internal/adapters/web/actions.templ +278 -0
| @@ -0,0 +1,278 @@ | |||
| 1 | + | package 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. | |
| 9 | + | templ CheckIcon(kind, state string) { | |
| 10 | + | <span class={ "ck", "ck-" + kind } role="img" aria-label={ state } title={ state }></span> | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | templ 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. | |
| 37 | + | templ 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 | + | templ 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 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // runBodyFragment is the body of the run page as a live fragment. | |
| 69 | + | templ runBodyFragment(p RunPage) { | |
| 70 | + | <div id="run-body"> | |
| 71 | + | @RunBody(p) | |
| 72 | + | </div> | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // RunBody is the jobs of a run, replaced by the live updates. | |
| 76 | + | templ RunBody(p RunPage) { | |
| 77 | + | <h2 class="run-h">@CheckIcon(p.Kind, p.State)<span>{ p.Workflow }</span> <span class="mute">{ p.Number }</span></h2> | |
| 78 | + | if p.Error != "" { | |
| 79 | + | <pre class="run-err">{ p.Error }</pre> | |
| 80 | + | } | |
| 81 | + | <ul class="jobs"> | |
| 82 | + | for _, j := range p.Jobs { | |
| 83 | + | <li id={ "job-" + j.ID }> | |
| 84 | + | <a class="job-h" href={ templ.SafeURL(j.Href) }> | |
| 85 | + | @CheckIcon(j.Kind, j.State) | |
| 86 | + | <span class="j-name">{ j.Name }</span> | |
| 87 | + | <span class="dim">{ j.State }{ " " }{ j.Duration }</span> | |
| 88 | + | </a> | |
| 89 | + | if j.Runner != "" { | |
| 90 | + | <span class="mute">on { runnerText(j) }</span> | |
| 91 | + | } | |
| 92 | + | if len(j.Needs) > 0 { | |
| 93 | + | <span class="mute">needs { joinWords(j.Needs) }</span> | |
| 94 | + | } | |
| 95 | + | if j.Error != "" { | |
| 96 | + | <p class="job-err">{ j.Error }</p> | |
| 97 | + | } | |
| 98 | + | if len(j.Steps) > 0 { | |
| 99 | + | <ol class="steps"> | |
| 100 | + | for _, s := range j.Steps { | |
| 101 | + | <li>@CheckIcon(s.Kind, s.State) <span>{ s.Name }</span> <span class="dim">{ s.Duration }</span></li> | |
| 102 | + | } | |
| 103 | + | </ol> | |
| 104 | + | } | |
| 105 | + | </li> | |
| 106 | + | } | |
| 107 | + | </ul> | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | templ runRail(p RunPage) { | |
| 111 | + | <section aria-labelledby="r-run"> | |
| 112 | + | <h2 id="r-run">Run</h2> | |
| 113 | + | <dl class="kv"> | |
| 114 | + | <dt>result</dt> | |
| 115 | + | <dd>{ p.State }</dd> | |
| 116 | + | <dt>commit</dt> | |
| 117 | + | <dd><a href={ templ.SafeURL(p.CHref) }>{ p.Short }</a></dd> | |
| 118 | + | <dt>ref</dt> | |
| 119 | + | <dd><a href={ templ.SafeURL(p.RefHref) }>{ p.Ref }</a></dd> | |
| 120 | + | <dt>actor</dt> | |
| 121 | + | <dd>{ p.Actor }</dd> | |
| 122 | + | <dt>attempt</dt> | |
| 123 | + | <dd>{ itoa(int(p.Attempt)) } of { itoa(int(p.Latest)) }</dd> | |
| 124 | + | </dl> | |
| 125 | + | if len(p.Attempts) > 0 { | |
| 126 | + | <ul class="attempts"> | |
| 127 | + | for _, a := range p.Attempts { | |
| 128 | + | <li> | |
| 129 | + | if a.Current { | |
| 130 | + | <span aria-current="page">{ a.Label }</span> | |
| 131 | + | } else { | |
| 132 | + | <a href={ templ.SafeURL(a.Href) }>{ a.Label }</a> | |
| 133 | + | } | |
| 134 | + | </li> | |
| 135 | + | } | |
| 136 | + | </ul> | |
| 137 | + | } | |
| 138 | + | </section> | |
| 139 | + | if p.CancelAction != "" || p.RerunAction != "" { | |
| 140 | + | <section aria-labelledby="r-act" class="decide"> | |
| 141 | + | <h2 id="r-act">Actions</h2> | |
| 142 | + | <div class="d-btns"> | |
| 143 | + | if p.CancelAction != "" { | |
| 144 | + | <form method="post" action={ templ.SafeURL(p.CancelAction) }><button type="submit" class="btn">Cancel run</button></form> | |
| 145 | + | } | |
| 146 | + | if p.RerunAction != "" { | |
| 147 | + | <form method="post" action={ templ.SafeURL(p.RerunAction) }><button type="submit" class="btn primary">Re-run all jobs</button></form> | |
| 148 | + | } | |
| 149 | + | if p.CanRerunFailed { | |
| 150 | + | <form method="post" action={ templ.SafeURL(p.RerunFailedAction) }><button type="submit" class="btn">Re-run failed jobs</button></form> | |
| 151 | + | } | |
| 152 | + | </div> | |
| 153 | + | </section> | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // JobHead is the title of the job page with its state and its error, replaced | |
| 158 | + | // by the live updates. total is the number of lines the log holds. | |
| 159 | + | templ JobHead(j JobRow, total int64) { | |
| 160 | + | <div id="job-head" data-total={ itoa64(total) }> | |
| 161 | + | <h2 class="run-h">@CheckIcon(j.Kind, j.State)<span>{ j.Name }</span> <span class="dim">{ j.State }{ " " }{ j.Duration }</span></h2> | |
| 162 | + | if j.Error != "" { | |
| 163 | + | <p class="job-err">{ j.Error }</p> | |
| 164 | + | } | |
| 165 | + | </div> | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // JobSteps is the steps of the job in the rail, replaced by the live updates. | |
| 169 | + | templ JobSteps(steps []StepView) { | |
| 170 | + | <ol class="steps" id="job-steps"> | |
| 171 | + | for _, s := range steps { | |
| 172 | + | <li> | |
| 173 | + | @CheckIcon(s.Kind, s.State) | |
| 174 | + | if s.LogEnd > s.LogStart { | |
| 175 | + | <a href={ templ.SafeURL("#L" + itoa64(s.LogStart)) }>{ s.Name }</a> | |
| 176 | + | } else { | |
| 177 | + | <span>{ s.Name }</span> | |
| 178 | + | } | |
| 179 | + | <span class="dim">{ s.Duration }</span> | |
| 180 | + | </li> | |
| 181 | + | } | |
| 182 | + | </ol> | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | templ Job(l Layout, p JobPage) { | |
| 186 | + | @Shell(l, jobRail(p)) { | |
| 187 | + | @LiveSlot("job-notice") | |
| 188 | + | @JobHead(p.JobRow, p.Total) | |
| 189 | + | <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))) }> | |
| 190 | + | if p.Note != "" { | |
| 191 | + | <p class="dim">{ p.Note }</p> | |
| 192 | + | } | |
| 193 | + | if p.Prev != "" { | |
| 194 | + | <p class="more"><a href={ templ.SafeURL(p.Prev) } rel="prev">earlier lines</a></p> | |
| 195 | + | } | |
| 196 | + | <ol class="log" start={ itoa64(p.From + 1) }> | |
| 197 | + | for _, ln := range p.Lines { | |
| 198 | + | @LogLine(ln) | |
| 199 | + | } | |
| 200 | + | </ol> | |
| 201 | + | if p.Next != "" { | |
| 202 | + | <p class="more"><a href={ templ.SafeURL(p.Next) } rel="next">more lines</a></p> | |
| 203 | + | } | |
| 204 | + | </div> | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // LogLine is a line of the log. Group and error markers of the runner show as | |
| 209 | + | // headings and errors, never as raw text. | |
| 210 | + | templ LogLine(ln LogLineView) { | |
| 211 | + | <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li> | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | templ jobRail(p JobPage) { | |
| 215 | + | <section aria-labelledby="r-job"> | |
| 216 | + | <h2 id="r-job">Steps</h2> | |
| 217 | + | @JobSteps(p.Steps) | |
| 218 | + | </section> | |
| 219 | + | <section aria-labelledby="r-jobf"> | |
| 220 | + | <h2 id="r-jobf">Job</h2> | |
| 221 | + | <dl class="kv"> | |
| 222 | + | <dt>run</dt> | |
| 223 | + | <dd><a href={ templ.SafeURL(p.RunHref) }>{ p.RunName }</a></dd> | |
| 224 | + | if p.Runner != "" { | |
| 225 | + | <dt>runner</dt> | |
| 226 | + | <dd>{ p.Runner }</dd> | |
| 227 | + | } | |
| 228 | + | if p.Isolation != "" { | |
| 229 | + | <dt>isolation</dt> | |
| 230 | + | <dd>{ p.Isolation }</dd> | |
| 231 | + | } | |
| 232 | + | <dt>lines</dt> | |
| 233 | + | <dd>{ itoa64(p.Total) }</dd> | |
| 234 | + | </dl> | |
| 235 | + | <p><a href={ templ.SafeURL(p.RawHref) }>raw log</a></p> | |
| 236 | + | </section> | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | // CommitChecks is the combined state of the checks of a commit, as a mark | |
| 240 | + | // with a count, linking to the page that explains it. | |
| 241 | + | templ CommitChecks(c ChecksSummary) { | |
| 242 | + | if c.Total > 0 { | |
| 243 | + | <a class="ck-sum" href={ templ.SafeURL(c.Href) } title={ c.Title }> | |
| 244 | + | @CheckIcon(c.Kind, c.State) | |
| 245 | + | <span class="mute">{ c.Label }</span> | |
| 246 | + | </a> | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // MRChecks is the checks section of the rail of a merge request. | |
| 251 | + | templ MRChecks(c ChecksSection) { | |
| 252 | + | <section aria-labelledby="r-checks" id="mr-checks"> | |
| 253 | + | <h2 id="r-checks">Checks <span class="count">{ itoa(len(c.Rows)) }</span></h2> | |
| 254 | + | if len(c.Rows) == 0 { | |
| 255 | + | <p class="dim">no checks</p> | |
| 256 | + | } else { | |
| 257 | + | <ul class="checks"> | |
| 258 | + | for _, r := range c.Rows { | |
| 259 | + | <li> | |
| 260 | + | @CheckIcon(r.Kind, r.State) | |
| 261 | + | if r.Href != "" { | |
| 262 | + | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | |
| 263 | + | } else { | |
| 264 | + | <span>{ r.Name }</span> | |
| 265 | + | } | |
| 266 | + | <span class="dim">{ r.State }</span> | |
| 267 | + | if r.Waiting { | |
| 268 | + | <span class="mute">waiting for a runner</span> | |
| 269 | + | } | |
| 270 | + | </li> | |
| 271 | + | } | |
| 272 | + | </ul> | |
| 273 | + | } | |
| 274 | + | if c.Note != "" { | |
| 275 | + | <p class="mute">{ c.Note }</p> | |
| 276 | + | } | |
| 277 | + | </section> | |
| 278 | + | } | |
A internal/adapters/web/actions_templ.go +2012 -0
| @@ -0,0 +1,2012 @@ | |||
| 1 | + | // Code generated by templ - DO NOT EDIT. | |
| 2 | + | ||
| 3 | + | // templ: version: v0.3.1020 | |
| 4 | + | package web | |
| 5 | + | ||
| 6 | + | //lint:file-ignore SA4006 This context is only used if a nested component is present. | |
| 7 | + | ||
| 8 | + | import "github.com/a-h/templ" | |
| 9 | + | import templruntime "github.com/a-h/templ/runtime" | |
| 10 | + | ||
| 11 | + | // The CI pages: the runs of a repository, a run with its jobs, a job with its | |
| 12 | + | // steps and its log (actions.go). A state is always a word as well as a | |
| 13 | + | // symbol and a colour. | |
| 14 | + | ||
| 15 | + | // CheckIcon is the symbol of a state: a mark in the colour of its kind, with | |
| 16 | + | // the state as its text for a screen reader. | |
| 17 | + | func CheckIcon(kind, state string) templ.Component { | |
| 18 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 19 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 20 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 21 | + | return templ_7745c5c3_CtxErr | |
| 22 | + | } | |
| 23 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 24 | + | if !templ_7745c5c3_IsBuffer { | |
| 25 | + | defer func() { | |
| 26 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 27 | + | if templ_7745c5c3_Err == nil { | |
| 28 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 29 | + | } | |
| 30 | + | }() | |
| 31 | + | } | |
| 32 | + | ctx = templ.InitializeContext(ctx) | |
| 33 | + | templ_7745c5c3_Var1 := templ.GetChildren(ctx) | |
| 34 | + | if templ_7745c5c3_Var1 == nil { | |
| 35 | + | templ_7745c5c3_Var1 = templ.NopComponent | |
| 36 | + | } | |
| 37 | + | ctx = templ.ClearChildren(ctx) | |
| 38 | + | var templ_7745c5c3_Var2 = []any{"ck", "ck-" + kind} | |
| 39 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) | |
| 40 | + | if templ_7745c5c3_Err != nil { | |
| 41 | + | return templ_7745c5c3_Err | |
| 42 | + | } | |
| 43 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<span class=\"") | |
| 44 | + | if templ_7745c5c3_Err != nil { | |
| 45 | + | return templ_7745c5c3_Err | |
| 46 | + | } | |
| 47 | + | var templ_7745c5c3_Var3 string | |
| 48 | + | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String()) | |
| 49 | + | if templ_7745c5c3_Err != nil { | |
| 50 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 1, Col: 0} | |
| 51 | + | } | |
| 52 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) | |
| 53 | + | if templ_7745c5c3_Err != nil { | |
| 54 | + | return templ_7745c5c3_Err | |
| 55 | + | } | |
| 56 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" role=\"img\" aria-label=\"") | |
| 57 | + | if templ_7745c5c3_Err != nil { | |
| 58 | + | return templ_7745c5c3_Err | |
| 59 | + | } | |
| 60 | + | var templ_7745c5c3_Var4 string | |
| 61 | + | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(state) | |
| 62 | + | if templ_7745c5c3_Err != nil { | |
| 63 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 10, Col: 65} | |
| 64 | + | } | |
| 65 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) | |
| 66 | + | if templ_7745c5c3_Err != nil { | |
| 67 | + | return templ_7745c5c3_Err | |
| 68 | + | } | |
| 69 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" title=\"") | |
| 70 | + | if templ_7745c5c3_Err != nil { | |
| 71 | + | return templ_7745c5c3_Err | |
| 72 | + | } | |
| 73 | + | var templ_7745c5c3_Var5 string | |
| 74 | + | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(state) | |
| 75 | + | if templ_7745c5c3_Err != nil { | |
| 76 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 10, Col: 81} | |
| 77 | + | } | |
| 78 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) | |
| 79 | + | if templ_7745c5c3_Err != nil { | |
| 80 | + | return templ_7745c5c3_Err | |
| 81 | + | } | |
| 82 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></span>") | |
| 83 | + | if templ_7745c5c3_Err != nil { | |
| 84 | + | return templ_7745c5c3_Err | |
| 85 | + | } | |
| 86 | + | return nil | |
| 87 | + | }) | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func Actions(l Layout, p ActionsPage) templ.Component { | |
| 91 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 92 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 93 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 94 | + | return templ_7745c5c3_CtxErr | |
| 95 | + | } | |
| 96 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 97 | + | if !templ_7745c5c3_IsBuffer { | |
| 98 | + | defer func() { | |
| 99 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 100 | + | if templ_7745c5c3_Err == nil { | |
| 101 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 102 | + | } | |
| 103 | + | }() | |
| 104 | + | } | |
| 105 | + | ctx = templ.InitializeContext(ctx) | |
| 106 | + | templ_7745c5c3_Var6 := templ.GetChildren(ctx) | |
| 107 | + | if templ_7745c5c3_Var6 == nil { | |
| 108 | + | templ_7745c5c3_Var6 = templ.NopComponent | |
| 109 | + | } | |
| 110 | + | ctx = templ.ClearChildren(ctx) | |
| 111 | + | templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 112 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 113 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 114 | + | if !templ_7745c5c3_IsBuffer { | |
| 115 | + | defer func() { | |
| 116 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 117 | + | if templ_7745c5c3_Err == nil { | |
| 118 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 119 | + | } | |
| 120 | + | }() | |
| 121 | + | } | |
| 122 | + | ctx = templ.InitializeContext(ctx) | |
| 123 | + | if len(p.Runs) == 0 { | |
| 124 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"dim\">no runs yet: a push that matches a workflow of .github/workflows starts one.</p>") | |
| 125 | + | if templ_7745c5c3_Err != nil { | |
| 126 | + | return templ_7745c5c3_Err | |
| 127 | + | } | |
| 128 | + | } else { | |
| 129 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table class=\"list runs\" aria-label=\"Runs\"><thead><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></thead> <tbody id=\"run-rows\" data-rows>") | |
| 130 | + | if templ_7745c5c3_Err != nil { | |
| 131 | + | return templ_7745c5c3_Err | |
| 132 | + | } | |
| 133 | + | for _, r := range p.Runs { | |
| 134 | + | templ_7745c5c3_Err = RunListRow(r).Render(ctx, templ_7745c5c3_Buffer) | |
| 135 | + | if templ_7745c5c3_Err != nil { | |
| 136 | + | return templ_7745c5c3_Err | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</tbody></table>") | |
| 140 | + | if templ_7745c5c3_Err != nil { | |
| 141 | + | return templ_7745c5c3_Err | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ") | |
| 145 | + | if templ_7745c5c3_Err != nil { | |
| 146 | + | return templ_7745c5c3_Err | |
| 147 | + | } | |
| 148 | + | if p.More != "" { | |
| 149 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"more\"><a href=\"") | |
| 150 | + | if templ_7745c5c3_Err != nil { | |
| 151 | + | return templ_7745c5c3_Err | |
| 152 | + | } | |
| 153 | + | var templ_7745c5c3_Var8 templ.SafeURL | |
| 154 | + | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.More)) | |
| 155 | + | if templ_7745c5c3_Err != nil { | |
| 156 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 30, Col: 50} | |
| 157 | + | } | |
| 158 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) | |
| 159 | + | if templ_7745c5c3_Err != nil { | |
| 160 | + | return templ_7745c5c3_Err | |
| 161 | + | } | |
| 162 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" rel=\"next\">older runs</a></p>") | |
| 163 | + | if templ_7745c5c3_Err != nil { | |
| 164 | + | return templ_7745c5c3_Err | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | return nil | |
| 168 | + | }) | |
| 169 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer) | |
| 170 | + | if templ_7745c5c3_Err != nil { | |
| 171 | + | return templ_7745c5c3_Err | |
| 172 | + | } | |
| 173 | + | return nil | |
| 174 | + | }) | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // RunListRow is a row of the list of runs; its id is the target of the live | |
| 178 | + | // updates. | |
| 179 | + | func RunListRow(r RunRow) templ.Component { | |
| 180 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 181 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 182 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 183 | + | return templ_7745c5c3_CtxErr | |
| 184 | + | } | |
| 185 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 186 | + | if !templ_7745c5c3_IsBuffer { | |
| 187 | + | defer func() { | |
| 188 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 189 | + | if templ_7745c5c3_Err == nil { | |
| 190 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 191 | + | } | |
| 192 | + | }() | |
| 193 | + | } | |
| 194 | + | ctx = templ.InitializeContext(ctx) | |
| 195 | + | templ_7745c5c3_Var9 := templ.GetChildren(ctx) | |
| 196 | + | if templ_7745c5c3_Var9 == nil { | |
| 197 | + | templ_7745c5c3_Var9 = templ.NopComponent | |
| 198 | + | } | |
| 199 | + | ctx = templ.ClearChildren(ctx) | |
| 200 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<tr id=\"") | |
| 201 | + | if templ_7745c5c3_Err != nil { | |
| 202 | + | return templ_7745c5c3_Err | |
| 203 | + | } | |
| 204 | + | var templ_7745c5c3_Var10 string | |
| 205 | + | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("run-" + r.ID) | |
| 206 | + | if templ_7745c5c3_Err != nil { | |
| 207 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 38, Col: 23} | |
| 208 | + | } | |
| 209 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | |
| 210 | + | if templ_7745c5c3_Err != nil { | |
| 211 | + | return templ_7745c5c3_Err | |
| 212 | + | } | |
| 213 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><td class=\"c-st\">") | |
| 214 | + | if templ_7745c5c3_Err != nil { | |
| 215 | + | return templ_7745c5c3_Err | |
| 216 | + | } | |
| 217 | + | templ_7745c5c3_Err = CheckIcon(r.Kind, r.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 218 | + | if templ_7745c5c3_Err != nil { | |
| 219 | + | return templ_7745c5c3_Err | |
| 220 | + | } | |
| 221 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td><td class=\"c-name\"><a href=\"") | |
| 222 | + | if templ_7745c5c3_Err != nil { | |
| 223 | + | return templ_7745c5c3_Err | |
| 224 | + | } | |
| 225 | + | var templ_7745c5c3_Var11 templ.SafeURL | |
| 226 | + | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 227 | + | if templ_7745c5c3_Err != nil { | |
| 228 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 41, Col: 34} | |
| 229 | + | } | |
| 230 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 231 | + | if templ_7745c5c3_Err != nil { | |
| 232 | + | return templ_7745c5c3_Err | |
| 233 | + | } | |
| 234 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">") | |
| 235 | + | if templ_7745c5c3_Err != nil { | |
| 236 | + | return templ_7745c5c3_Err | |
| 237 | + | } | |
| 238 | + | var templ_7745c5c3_Var12 string | |
| 239 | + | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow) | |
| 240 | + | if templ_7745c5c3_Err != nil { | |
| 241 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 41, Col: 49} | |
| 242 | + | } | |
| 243 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | |
| 244 | + | if templ_7745c5c3_Err != nil { | |
| 245 | + | return templ_7745c5c3_Err | |
| 246 | + | } | |
| 247 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a> <span class=\"mute\">") | |
| 248 | + | if templ_7745c5c3_Err != nil { | |
| 249 | + | return templ_7745c5c3_Err | |
| 250 | + | } | |
| 251 | + | var templ_7745c5c3_Var13 string | |
| 252 | + | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number) | |
| 253 | + | if templ_7745c5c3_Err != nil { | |
| 254 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 42, Col: 32} | |
| 255 | + | } | |
| 256 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 257 | + | if templ_7745c5c3_Err != nil { | |
| 258 | + | return templ_7745c5c3_Err | |
| 259 | + | } | |
| 260 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> ") | |
| 261 | + | if templ_7745c5c3_Err != nil { | |
| 262 | + | return templ_7745c5c3_Err | |
| 263 | + | } | |
| 264 | + | if r.Attempt > 1 { | |
| 265 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"pill\">attempt ") | |
| 266 | + | if templ_7745c5c3_Err != nil { | |
| 267 | + | return templ_7745c5c3_Err | |
| 268 | + | } | |
| 269 | + | var templ_7745c5c3_Var14 string | |
| 270 | + | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(r.Attempt))) | |
| 271 | + | if templ_7745c5c3_Err != nil { | |
| 272 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 44, Col: 53} | |
| 273 | + | } | |
| 274 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |
| 275 | + | if templ_7745c5c3_Err != nil { | |
| 276 | + | return templ_7745c5c3_Err | |
| 277 | + | } | |
| 278 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span>") | |
| 279 | + | if templ_7745c5c3_Err != nil { | |
| 280 | + | return templ_7745c5c3_Err | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"c-sub\"><a href=\"") | |
| 284 | + | if templ_7745c5c3_Err != nil { | |
| 285 | + | return templ_7745c5c3_Err | |
| 286 | + | } | |
| 287 | + | var templ_7745c5c3_Var15 templ.SafeURL | |
| 288 | + | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref)) | |
| 289 | + | if templ_7745c5c3_Err != nil { | |
| 290 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 47, Col: 52} | |
| 291 | + | } | |
| 292 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | |
| 293 | + | if templ_7745c5c3_Err != nil { | |
| 294 | + | return templ_7745c5c3_Err | |
| 295 | + | } | |
| 296 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") | |
| 297 | + | if templ_7745c5c3_Err != nil { | |
| 298 | + | return templ_7745c5c3_Err | |
| 299 | + | } | |
| 300 | + | var templ_7745c5c3_Var16 string | |
| 301 | + | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | |
| 302 | + | if templ_7745c5c3_Err != nil { | |
| 303 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 47, Col: 64} | |
| 304 | + | } | |
| 305 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 306 | + | if templ_7745c5c3_Err != nil { | |
| 307 | + | return templ_7745c5c3_Err | |
| 308 | + | } | |
| 309 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</a> ") | |
| 310 | + | if templ_7745c5c3_Err != nil { | |
| 311 | + | return templ_7745c5c3_Err | |
| 312 | + | } | |
| 313 | + | var templ_7745c5c3_Var17 string | |
| 314 | + | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) | |
| 315 | + | if templ_7745c5c3_Err != nil { | |
| 316 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 47, Col: 80} | |
| 317 | + | } | |
| 318 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | |
| 319 | + | if templ_7745c5c3_Err != nil { | |
| 320 | + | return templ_7745c5c3_Err | |
| 321 | + | } | |
| 322 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td><td class=\"c-ref\"><a href=\"") | |
| 323 | + | if templ_7745c5c3_Err != nil { | |
| 324 | + | return templ_7745c5c3_Err | |
| 325 | + | } | |
| 326 | + | var templ_7745c5c3_Var18 templ.SafeURL | |
| 327 | + | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RefHref)) | |
| 328 | + | if templ_7745c5c3_Err != nil { | |
| 329 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 48, Col: 54} | |
| 330 | + | } | |
| 331 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | |
| 332 | + | if templ_7745c5c3_Err != nil { | |
| 333 | + | return templ_7745c5c3_Err | |
| 334 | + | } | |
| 335 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">") | |
| 336 | + | if templ_7745c5c3_Err != nil { | |
| 337 | + | return templ_7745c5c3_Err | |
| 338 | + | } | |
| 339 | + | var templ_7745c5c3_Var19 string | |
| 340 | + | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref) | |
| 341 | + | if templ_7745c5c3_Err != nil { | |
| 342 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 48, Col: 64} | |
| 343 | + | } | |
| 344 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 345 | + | if templ_7745c5c3_Err != nil { | |
| 346 | + | return templ_7745c5c3_Err | |
| 347 | + | } | |
| 348 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</a></td><td class=\"c-who dim\">") | |
| 349 | + | if templ_7745c5c3_Err != nil { | |
| 350 | + | return templ_7745c5c3_Err | |
| 351 | + | } | |
| 352 | + | var templ_7745c5c3_Var20 string | |
| 353 | + | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(r.Actor) | |
| 354 | + | if templ_7745c5c3_Err != nil { | |
| 355 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 49, Col: 33} | |
| 356 | + | } | |
| 357 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | |
| 358 | + | if templ_7745c5c3_Err != nil { | |
| 359 | + | return templ_7745c5c3_Err | |
| 360 | + | } | |
| 361 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td class=\"c-dur dim\">") | |
| 362 | + | if templ_7745c5c3_Err != nil { | |
| 363 | + | return templ_7745c5c3_Err | |
| 364 | + | } | |
| 365 | + | var templ_7745c5c3_Var21 string | |
| 366 | + | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(r.State) | |
| 367 | + | if templ_7745c5c3_Err != nil { | |
| 368 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 50, Col: 33} | |
| 369 | + | } | |
| 370 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | |
| 371 | + | if templ_7745c5c3_Err != nil { | |
| 372 | + | return templ_7745c5c3_Err | |
| 373 | + | } | |
| 374 | + | var templ_7745c5c3_Var22 string | |
| 375 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 376 | + | if templ_7745c5c3_Err != nil { | |
| 377 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 50, Col: 40} | |
| 378 | + | } | |
| 379 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 380 | + | if templ_7745c5c3_Err != nil { | |
| 381 | + | return templ_7745c5c3_Err | |
| 382 | + | } | |
| 383 | + | var templ_7745c5c3_Var23 string | |
| 384 | + | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(r.Duration) | |
| 385 | + | if templ_7745c5c3_Err != nil { | |
| 386 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 50, Col: 54} | |
| 387 | + | } | |
| 388 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) | |
| 389 | + | if templ_7745c5c3_Err != nil { | |
| 390 | + | return templ_7745c5c3_Err | |
| 391 | + | } | |
| 392 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td class=\"c-when dim\">") | |
| 393 | + | if templ_7745c5c3_Err != nil { | |
| 394 | + | return templ_7745c5c3_Err | |
| 395 | + | } | |
| 396 | + | if r.WhenAt != "" { | |
| 397 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<time datetime=\"") | |
| 398 | + | if templ_7745c5c3_Err != nil { | |
| 399 | + | return templ_7745c5c3_Err | |
| 400 | + | } | |
| 401 | + | var templ_7745c5c3_Var24 string | |
| 402 | + | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 403 | + | if templ_7745c5c3_Err != nil { | |
| 404 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 53, Col: 29} | |
| 405 | + | } | |
| 406 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | |
| 407 | + | if templ_7745c5c3_Err != nil { | |
| 408 | + | return templ_7745c5c3_Err | |
| 409 | + | } | |
| 410 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" title=\"") | |
| 411 | + | if templ_7745c5c3_Err != nil { | |
| 412 | + | return templ_7745c5c3_Err | |
| 413 | + | } | |
| 414 | + | var templ_7745c5c3_Var25 string | |
| 415 | + | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 416 | + | if templ_7745c5c3_Err != nil { | |
| 417 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 53, Col: 48} | |
| 418 | + | } | |
| 419 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) | |
| 420 | + | if templ_7745c5c3_Err != nil { | |
| 421 | + | return templ_7745c5c3_Err | |
| 422 | + | } | |
| 423 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">") | |
| 424 | + | if templ_7745c5c3_Err != nil { | |
| 425 | + | return templ_7745c5c3_Err | |
| 426 | + | } | |
| 427 | + | var templ_7745c5c3_Var26 string | |
| 428 | + | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | |
| 429 | + | if templ_7745c5c3_Err != nil { | |
| 430 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 53, Col: 59} | |
| 431 | + | } | |
| 432 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 433 | + | if templ_7745c5c3_Err != nil { | |
| 434 | + | return templ_7745c5c3_Err | |
| 435 | + | } | |
| 436 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</time>") | |
| 437 | + | if templ_7745c5c3_Err != nil { | |
| 438 | + | return templ_7745c5c3_Err | |
| 439 | + | } | |
| 440 | + | } | |
| 441 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td></tr>") | |
| 442 | + | if templ_7745c5c3_Err != nil { | |
| 443 | + | return templ_7745c5c3_Err | |
| 444 | + | } | |
| 445 | + | return nil | |
| 446 | + | }) | |
| 447 | + | } | |
| 448 | + | ||
| 449 | + | func RunDetail(l Layout, p RunPage) templ.Component { | |
| 450 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 451 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 452 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 453 | + | return templ_7745c5c3_CtxErr | |
| 454 | + | } | |
| 455 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 456 | + | if !templ_7745c5c3_IsBuffer { | |
| 457 | + | defer func() { | |
| 458 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 459 | + | if templ_7745c5c3_Err == nil { | |
| 460 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 461 | + | } | |
| 462 | + | }() | |
| 463 | + | } | |
| 464 | + | ctx = templ.InitializeContext(ctx) | |
| 465 | + | templ_7745c5c3_Var27 := templ.GetChildren(ctx) | |
| 466 | + | if templ_7745c5c3_Var27 == nil { | |
| 467 | + | templ_7745c5c3_Var27 = templ.NopComponent | |
| 468 | + | } | |
| 469 | + | ctx = templ.ClearChildren(ctx) | |
| 470 | + | templ_7745c5c3_Var28 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 471 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 472 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 473 | + | if !templ_7745c5c3_IsBuffer { | |
| 474 | + | defer func() { | |
| 475 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 476 | + | if templ_7745c5c3_Err == nil { | |
| 477 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 478 | + | } | |
| 479 | + | }() | |
| 480 | + | } | |
| 481 | + | ctx = templ.InitializeContext(ctx) | |
| 482 | + | templ_7745c5c3_Err = LiveSlot("run-notice").Render(ctx, templ_7745c5c3_Buffer) | |
| 483 | + | if templ_7745c5c3_Err != nil { | |
| 484 | + | return templ_7745c5c3_Err | |
| 485 | + | } | |
| 486 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " <div id=\"run-body\">") | |
| 487 | + | if templ_7745c5c3_Err != nil { | |
| 488 | + | return templ_7745c5c3_Err | |
| 489 | + | } | |
| 490 | + | templ_7745c5c3_Err = RunBody(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 491 | + | if templ_7745c5c3_Err != nil { | |
| 492 | + | return templ_7745c5c3_Err | |
| 493 | + | } | |
| 494 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>") | |
| 495 | + | if templ_7745c5c3_Err != nil { | |
| 496 | + | return templ_7745c5c3_Err | |
| 497 | + | } | |
| 498 | + | return nil | |
| 499 | + | }) | |
| 500 | + | templ_7745c5c3_Err = Shell(l, runRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var28), templ_7745c5c3_Buffer) | |
| 501 | + | if templ_7745c5c3_Err != nil { | |
| 502 | + | return templ_7745c5c3_Err | |
| 503 | + | } | |
| 504 | + | return nil | |
| 505 | + | }) | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // runBodyFragment is the body of the run page as a live fragment. | |
| 509 | + | func runBodyFragment(p RunPage) templ.Component { | |
| 510 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 511 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 512 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 513 | + | return templ_7745c5c3_CtxErr | |
| 514 | + | } | |
| 515 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 516 | + | if !templ_7745c5c3_IsBuffer { | |
| 517 | + | defer func() { | |
| 518 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 519 | + | if templ_7745c5c3_Err == nil { | |
| 520 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 521 | + | } | |
| 522 | + | }() | |
| 523 | + | } | |
| 524 | + | ctx = templ.InitializeContext(ctx) | |
| 525 | + | templ_7745c5c3_Var29 := templ.GetChildren(ctx) | |
| 526 | + | if templ_7745c5c3_Var29 == nil { | |
| 527 | + | templ_7745c5c3_Var29 = templ.NopComponent | |
| 528 | + | } | |
| 529 | + | ctx = templ.ClearChildren(ctx) | |
| 530 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div id=\"run-body\">") | |
| 531 | + | if templ_7745c5c3_Err != nil { | |
| 532 | + | return templ_7745c5c3_Err | |
| 533 | + | } | |
| 534 | + | templ_7745c5c3_Err = RunBody(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 535 | + | if templ_7745c5c3_Err != nil { | |
| 536 | + | return templ_7745c5c3_Err | |
| 537 | + | } | |
| 538 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>") | |
| 539 | + | if templ_7745c5c3_Err != nil { | |
| 540 | + | return templ_7745c5c3_Err | |
| 541 | + | } | |
| 542 | + | return nil | |
| 543 | + | }) | |
| 544 | + | } | |
| 545 | + | ||
| 546 | + | // RunBody is the jobs of a run, replaced by the live updates. | |
| 547 | + | func RunBody(p RunPage) templ.Component { | |
| 548 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 549 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 550 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 551 | + | return templ_7745c5c3_CtxErr | |
| 552 | + | } | |
| 553 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 554 | + | if !templ_7745c5c3_IsBuffer { | |
| 555 | + | defer func() { | |
| 556 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 557 | + | if templ_7745c5c3_Err == nil { | |
| 558 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 559 | + | } | |
| 560 | + | }() | |
| 561 | + | } | |
| 562 | + | ctx = templ.InitializeContext(ctx) | |
| 563 | + | templ_7745c5c3_Var30 := templ.GetChildren(ctx) | |
| 564 | + | if templ_7745c5c3_Var30 == nil { | |
| 565 | + | templ_7745c5c3_Var30 = templ.NopComponent | |
| 566 | + | } | |
| 567 | + | ctx = templ.ClearChildren(ctx) | |
| 568 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<h2 class=\"run-h\">") | |
| 569 | + | if templ_7745c5c3_Err != nil { | |
| 570 | + | return templ_7745c5c3_Err | |
| 571 | + | } | |
| 572 | + | templ_7745c5c3_Err = CheckIcon(p.Kind, p.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 573 | + | if templ_7745c5c3_Err != nil { | |
| 574 | + | return templ_7745c5c3_Err | |
| 575 | + | } | |
| 576 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span>") | |
| 577 | + | if templ_7745c5c3_Err != nil { | |
| 578 | + | return templ_7745c5c3_Err | |
| 579 | + | } | |
| 580 | + | var templ_7745c5c3_Var31 string | |
| 581 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.Workflow) | |
| 582 | + | if templ_7745c5c3_Err != nil { | |
| 583 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 77, Col: 64} | |
| 584 | + | } | |
| 585 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 586 | + | if templ_7745c5c3_Err != nil { | |
| 587 | + | return templ_7745c5c3_Err | |
| 588 | + | } | |
| 589 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span> <span class=\"mute\">") | |
| 590 | + | if templ_7745c5c3_Err != nil { | |
| 591 | + | return templ_7745c5c3_Err | |
| 592 | + | } | |
| 593 | + | var templ_7745c5c3_Var32 string | |
| 594 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number) | |
| 595 | + | if templ_7745c5c3_Err != nil { | |
| 596 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 77, Col: 103} | |
| 597 | + | } | |
| 598 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 599 | + | if templ_7745c5c3_Err != nil { | |
| 600 | + | return templ_7745c5c3_Err | |
| 601 | + | } | |
| 602 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span></h2>") | |
| 603 | + | if templ_7745c5c3_Err != nil { | |
| 604 | + | return templ_7745c5c3_Err | |
| 605 | + | } | |
| 606 | + | if p.Error != "" { | |
| 607 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<pre class=\"run-err\">") | |
| 608 | + | if templ_7745c5c3_Err != nil { | |
| 609 | + | return templ_7745c5c3_Err | |
| 610 | + | } | |
| 611 | + | var templ_7745c5c3_Var33 string | |
| 612 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(p.Error) | |
| 613 | + | if templ_7745c5c3_Err != nil { | |
| 614 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 79, Col: 32} | |
| 615 | + | } | |
| 616 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 617 | + | if templ_7745c5c3_Err != nil { | |
| 618 | + | return templ_7745c5c3_Err | |
| 619 | + | } | |
| 620 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</pre>") | |
| 621 | + | if templ_7745c5c3_Err != nil { | |
| 622 | + | return templ_7745c5c3_Err | |
| 623 | + | } | |
| 624 | + | } | |
| 625 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<ul class=\"jobs\">") | |
| 626 | + | if templ_7745c5c3_Err != nil { | |
| 627 | + | return templ_7745c5c3_Err | |
| 628 | + | } | |
| 629 | + | for _, j := range p.Jobs { | |
| 630 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<li id=\"") | |
| 631 | + | if templ_7745c5c3_Err != nil { | |
| 632 | + | return templ_7745c5c3_Err | |
| 633 | + | } | |
| 634 | + | var templ_7745c5c3_Var34 string | |
| 635 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue("job-" + j.ID) | |
| 636 | + | if templ_7745c5c3_Err != nil { | |
| 637 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 83, Col: 25} | |
| 638 | + | } | |
| 639 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34) | |
| 640 | + | if templ_7745c5c3_Err != nil { | |
| 641 | + | return templ_7745c5c3_Err | |
| 642 | + | } | |
| 643 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><a class=\"job-h\" href=\"") | |
| 644 | + | if templ_7745c5c3_Err != nil { | |
| 645 | + | return templ_7745c5c3_Err | |
| 646 | + | } | |
| 647 | + | var templ_7745c5c3_Var35 templ.SafeURL | |
| 648 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(j.Href)) | |
| 649 | + | if templ_7745c5c3_Err != nil { | |
| 650 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 84, Col: 49} | |
| 651 | + | } | |
| 652 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 653 | + | if templ_7745c5c3_Err != nil { | |
| 654 | + | return templ_7745c5c3_Err | |
| 655 | + | } | |
| 656 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">") | |
| 657 | + | if templ_7745c5c3_Err != nil { | |
| 658 | + | return templ_7745c5c3_Err | |
| 659 | + | } | |
| 660 | + | templ_7745c5c3_Err = CheckIcon(j.Kind, j.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 661 | + | if templ_7745c5c3_Err != nil { | |
| 662 | + | return templ_7745c5c3_Err | |
| 663 | + | } | |
| 664 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<span class=\"j-name\">") | |
| 665 | + | if templ_7745c5c3_Err != nil { | |
| 666 | + | return templ_7745c5c3_Err | |
| 667 | + | } | |
| 668 | + | var templ_7745c5c3_Var36 string | |
| 669 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(j.Name) | |
| 670 | + | if templ_7745c5c3_Err != nil { | |
| 671 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 86, Col: 34} | |
| 672 | + | } | |
| 673 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 674 | + | if templ_7745c5c3_Err != nil { | |
| 675 | + | return templ_7745c5c3_Err | |
| 676 | + | } | |
| 677 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span> <span class=\"dim\">") | |
| 678 | + | if templ_7745c5c3_Err != nil { | |
| 679 | + | return templ_7745c5c3_Err | |
| 680 | + | } | |
| 681 | + | var templ_7745c5c3_Var37 string | |
| 682 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(j.State) | |
| 683 | + | if templ_7745c5c3_Err != nil { | |
| 684 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 87, Col: 32} | |
| 685 | + | } | |
| 686 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 687 | + | if templ_7745c5c3_Err != nil { | |
| 688 | + | return templ_7745c5c3_Err | |
| 689 | + | } | |
| 690 | + | var templ_7745c5c3_Var38 string | |
| 691 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 692 | + | if templ_7745c5c3_Err != nil { | |
| 693 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 87, Col: 39} | |
| 694 | + | } | |
| 695 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | |
| 696 | + | if templ_7745c5c3_Err != nil { | |
| 697 | + | return templ_7745c5c3_Err | |
| 698 | + | } | |
| 699 | + | var templ_7745c5c3_Var39 string | |
| 700 | + | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(j.Duration) | |
| 701 | + | if templ_7745c5c3_Err != nil { | |
| 702 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 87, Col: 53} | |
| 703 | + | } | |
| 704 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 705 | + | if templ_7745c5c3_Err != nil { | |
| 706 | + | return templ_7745c5c3_Err | |
| 707 | + | } | |
| 708 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></a> ") | |
| 709 | + | if templ_7745c5c3_Err != nil { | |
| 710 | + | return templ_7745c5c3_Err | |
| 711 | + | } | |
| 712 | + | if j.Runner != "" { | |
| 713 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"mute\">on ") | |
| 714 | + | if templ_7745c5c3_Err != nil { | |
| 715 | + | return templ_7745c5c3_Err | |
| 716 | + | } | |
| 717 | + | var templ_7745c5c3_Var40 string | |
| 718 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(runnerText(j)) | |
| 719 | + | if templ_7745c5c3_Err != nil { | |
| 720 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 90, Col: 42} | |
| 721 | + | } | |
| 722 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 723 | + | if templ_7745c5c3_Err != nil { | |
| 724 | + | return templ_7745c5c3_Err | |
| 725 | + | } | |
| 726 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span> ") | |
| 727 | + | if templ_7745c5c3_Err != nil { | |
| 728 | + | return templ_7745c5c3_Err | |
| 729 | + | } | |
| 730 | + | } | |
| 731 | + | if len(j.Needs) > 0 { | |
| 732 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<span class=\"mute\">needs ") | |
| 733 | + | if templ_7745c5c3_Err != nil { | |
| 734 | + | return templ_7745c5c3_Err | |
| 735 | + | } | |
| 736 | + | var templ_7745c5c3_Var41 string | |
| 737 | + | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(joinWords(j.Needs)) | |
| 738 | + | if templ_7745c5c3_Err != nil { | |
| 739 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 93, Col: 50} | |
| 740 | + | } | |
| 741 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 742 | + | if templ_7745c5c3_Err != nil { | |
| 743 | + | return templ_7745c5c3_Err | |
| 744 | + | } | |
| 745 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</span> ") | |
| 746 | + | if templ_7745c5c3_Err != nil { | |
| 747 | + | return templ_7745c5c3_Err | |
| 748 | + | } | |
| 749 | + | } | |
| 750 | + | if j.Error != "" { | |
| 751 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<p class=\"job-err\">") | |
| 752 | + | if templ_7745c5c3_Err != nil { | |
| 753 | + | return templ_7745c5c3_Err | |
| 754 | + | } | |
| 755 | + | var templ_7745c5c3_Var42 string | |
| 756 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(j.Error) | |
| 757 | + | if templ_7745c5c3_Err != nil { | |
| 758 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 96, Col: 33} | |
| 759 | + | } | |
| 760 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 761 | + | if templ_7745c5c3_Err != nil { | |
| 762 | + | return templ_7745c5c3_Err | |
| 763 | + | } | |
| 764 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</p>") | |
| 765 | + | if templ_7745c5c3_Err != nil { | |
| 766 | + | return templ_7745c5c3_Err | |
| 767 | + | } | |
| 768 | + | } | |
| 769 | + | if len(j.Steps) > 0 { | |
| 770 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<ol class=\"steps\">") | |
| 771 | + | if templ_7745c5c3_Err != nil { | |
| 772 | + | return templ_7745c5c3_Err | |
| 773 | + | } | |
| 774 | + | for _, s := range j.Steps { | |
| 775 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<li>") | |
| 776 | + | if templ_7745c5c3_Err != nil { | |
| 777 | + | return templ_7745c5c3_Err | |
| 778 | + | } | |
| 779 | + | templ_7745c5c3_Err = CheckIcon(s.Kind, s.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 780 | + | if templ_7745c5c3_Err != nil { | |
| 781 | + | return templ_7745c5c3_Err | |
| 782 | + | } | |
| 783 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<span>") | |
| 784 | + | if templ_7745c5c3_Err != nil { | |
| 785 | + | return templ_7745c5c3_Err | |
| 786 | + | } | |
| 787 | + | var templ_7745c5c3_Var43 string | |
| 788 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name) | |
| 789 | + | if templ_7745c5c3_Err != nil { | |
| 790 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 101, Col: 53} | |
| 791 | + | } | |
| 792 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 793 | + | if templ_7745c5c3_Err != nil { | |
| 794 | + | return templ_7745c5c3_Err | |
| 795 | + | } | |
| 796 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span> <span class=\"dim\">") | |
| 797 | + | if templ_7745c5c3_Err != nil { | |
| 798 | + | return templ_7745c5c3_Err | |
| 799 | + | } | |
| 800 | + | var templ_7745c5c3_Var44 string | |
| 801 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(s.Duration) | |
| 802 | + | if templ_7745c5c3_Err != nil { | |
| 803 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 101, Col: 93} | |
| 804 | + | } | |
| 805 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 806 | + | if templ_7745c5c3_Err != nil { | |
| 807 | + | return templ_7745c5c3_Err | |
| 808 | + | } | |
| 809 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span></li>") | |
| 810 | + | if templ_7745c5c3_Err != nil { | |
| 811 | + | return templ_7745c5c3_Err | |
| 812 | + | } | |
| 813 | + | } | |
| 814 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</ol>") | |
| 815 | + | if templ_7745c5c3_Err != nil { | |
| 816 | + | return templ_7745c5c3_Err | |
| 817 | + | } | |
| 818 | + | } | |
| 819 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</li>") | |
| 820 | + | if templ_7745c5c3_Err != nil { | |
| 821 | + | return templ_7745c5c3_Err | |
| 822 | + | } | |
| 823 | + | } | |
| 824 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</ul>") | |
| 825 | + | if templ_7745c5c3_Err != nil { | |
| 826 | + | return templ_7745c5c3_Err | |
| 827 | + | } | |
| 828 | + | return nil | |
| 829 | + | }) | |
| 830 | + | } | |
| 831 | + | ||
| 832 | + | func runRail(p RunPage) templ.Component { | |
| 833 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 834 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 835 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 836 | + | return templ_7745c5c3_CtxErr | |
| 837 | + | } | |
| 838 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 839 | + | if !templ_7745c5c3_IsBuffer { | |
| 840 | + | defer func() { | |
| 841 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 842 | + | if templ_7745c5c3_Err == nil { | |
| 843 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 844 | + | } | |
| 845 | + | }() | |
| 846 | + | } | |
| 847 | + | ctx = templ.InitializeContext(ctx) | |
| 848 | + | templ_7745c5c3_Var45 := templ.GetChildren(ctx) | |
| 849 | + | if templ_7745c5c3_Var45 == nil { | |
| 850 | + | templ_7745c5c3_Var45 = templ.NopComponent | |
| 851 | + | } | |
| 852 | + | ctx = templ.ClearChildren(ctx) | |
| 853 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<section aria-labelledby=\"r-run\"><h2 id=\"r-run\">Run</h2><dl class=\"kv\"><dt>result</dt><dd>") | |
| 854 | + | if templ_7745c5c3_Err != nil { | |
| 855 | + | return templ_7745c5c3_Err | |
| 856 | + | } | |
| 857 | + | var templ_7745c5c3_Var46 string | |
| 858 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.State) | |
| 859 | + | if templ_7745c5c3_Err != nil { | |
| 860 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 115, Col: 16} | |
| 861 | + | } | |
| 862 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 863 | + | if templ_7745c5c3_Err != nil { | |
| 864 | + | return templ_7745c5c3_Err | |
| 865 | + | } | |
| 866 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</dd><dt>commit</dt><dd><a href=\"") | |
| 867 | + | if templ_7745c5c3_Err != nil { | |
| 868 | + | return templ_7745c5c3_Err | |
| 869 | + | } | |
| 870 | + | var templ_7745c5c3_Var47 templ.SafeURL | |
| 871 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CHref)) | |
| 872 | + | if templ_7745c5c3_Err != nil { | |
| 873 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 117, Col: 39} | |
| 874 | + | } | |
| 875 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | |
| 876 | + | if templ_7745c5c3_Err != nil { | |
| 877 | + | return templ_7745c5c3_Err | |
| 878 | + | } | |
| 879 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">") | |
| 880 | + | if templ_7745c5c3_Err != nil { | |
| 881 | + | return templ_7745c5c3_Err | |
| 882 | + | } | |
| 883 | + | var templ_7745c5c3_Var48 string | |
| 884 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Short) | |
| 885 | + | if templ_7745c5c3_Err != nil { | |
| 886 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 117, Col: 51} | |
| 887 | + | } | |
| 888 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 889 | + | if templ_7745c5c3_Err != nil { | |
| 890 | + | return templ_7745c5c3_Err | |
| 891 | + | } | |
| 892 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</a></dd><dt>ref</dt><dd><a href=\"") | |
| 893 | + | if templ_7745c5c3_Err != nil { | |
| 894 | + | return templ_7745c5c3_Err | |
| 895 | + | } | |
| 896 | + | var templ_7745c5c3_Var49 templ.SafeURL | |
| 897 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RefHref)) | |
| 898 | + | if templ_7745c5c3_Err != nil { | |
| 899 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 119, Col: 41} | |
| 900 | + | } | |
| 901 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 902 | + | if templ_7745c5c3_Err != nil { | |
| 903 | + | return templ_7745c5c3_Err | |
| 904 | + | } | |
| 905 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") | |
| 906 | + | if templ_7745c5c3_Err != nil { | |
| 907 | + | return templ_7745c5c3_Err | |
| 908 | + | } | |
| 909 | + | var templ_7745c5c3_Var50 string | |
| 910 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Ref) | |
| 911 | + | if templ_7745c5c3_Err != nil { | |
| 912 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 119, Col: 51} | |
| 913 | + | } | |
| 914 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | |
| 915 | + | if templ_7745c5c3_Err != nil { | |
| 916 | + | return templ_7745c5c3_Err | |
| 917 | + | } | |
| 918 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</a></dd><dt>actor</dt><dd>") | |
| 919 | + | if templ_7745c5c3_Err != nil { | |
| 920 | + | return templ_7745c5c3_Err | |
| 921 | + | } | |
| 922 | + | var templ_7745c5c3_Var51 string | |
| 923 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(p.Actor) | |
| 924 | + | if templ_7745c5c3_Err != nil { | |
| 925 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 121, Col: 16} | |
| 926 | + | } | |
| 927 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 928 | + | if templ_7745c5c3_Err != nil { | |
| 929 | + | return templ_7745c5c3_Err | |
| 930 | + | } | |
| 931 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</dd><dt>attempt</dt><dd>") | |
| 932 | + | if templ_7745c5c3_Err != nil { | |
| 933 | + | return templ_7745c5c3_Err | |
| 934 | + | } | |
| 935 | + | var templ_7745c5c3_Var52 string | |
| 936 | + | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(p.Attempt))) | |
| 937 | + | if templ_7745c5c3_Err != nil { | |
| 938 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 123, Col: 29} | |
| 939 | + | } | |
| 940 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | |
| 941 | + | if templ_7745c5c3_Err != nil { | |
| 942 | + | return templ_7745c5c3_Err | |
| 943 | + | } | |
| 944 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " of ") | |
| 945 | + | if templ_7745c5c3_Err != nil { | |
| 946 | + | return templ_7745c5c3_Err | |
| 947 | + | } | |
| 948 | + | var templ_7745c5c3_Var53 string | |
| 949 | + | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(p.Latest))) | |
| 950 | + | if templ_7745c5c3_Err != nil { | |
| 951 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 123, Col: 56} | |
| 952 | + | } | |
| 953 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) | |
| 954 | + | if templ_7745c5c3_Err != nil { | |
| 955 | + | return templ_7745c5c3_Err | |
| 956 | + | } | |
| 957 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</dd></dl>") | |
| 958 | + | if templ_7745c5c3_Err != nil { | |
| 959 | + | return templ_7745c5c3_Err | |
| 960 | + | } | |
| 961 | + | if len(p.Attempts) > 0 { | |
| 962 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<ul class=\"attempts\">") | |
| 963 | + | if templ_7745c5c3_Err != nil { | |
| 964 | + | return templ_7745c5c3_Err | |
| 965 | + | } | |
| 966 | + | for _, a := range p.Attempts { | |
| 967 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<li>") | |
| 968 | + | if templ_7745c5c3_Err != nil { | |
| 969 | + | return templ_7745c5c3_Err | |
| 970 | + | } | |
| 971 | + | if a.Current { | |
| 972 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<span aria-current=\"page\">") | |
| 973 | + | if templ_7745c5c3_Err != nil { | |
| 974 | + | return templ_7745c5c3_Err | |
| 975 | + | } | |
| 976 | + | var templ_7745c5c3_Var54 string | |
| 977 | + | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(a.Label) | |
| 978 | + | if templ_7745c5c3_Err != nil { | |
| 979 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 130, Col: 42} | |
| 980 | + | } | |
| 981 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 982 | + | if templ_7745c5c3_Err != nil { | |
| 983 | + | return templ_7745c5c3_Err | |
| 984 | + | } | |
| 985 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</span>") | |
| 986 | + | if templ_7745c5c3_Err != nil { | |
| 987 | + | return templ_7745c5c3_Err | |
| 988 | + | } | |
| 989 | + | } else { | |
| 990 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<a href=\"") | |
| 991 | + | if templ_7745c5c3_Err != nil { | |
| 992 | + | return templ_7745c5c3_Err | |
| 993 | + | } | |
| 994 | + | var templ_7745c5c3_Var55 templ.SafeURL | |
| 995 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.Href)) | |
| 996 | + | if templ_7745c5c3_Err != nil { | |
| 997 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 132, Col: 38} | |
| 998 | + | } | |
| 999 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1000 | + | if templ_7745c5c3_Err != nil { | |
| 1001 | + | return templ_7745c5c3_Err | |
| 1002 | + | } | |
| 1003 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\">") | |
| 1004 | + | if templ_7745c5c3_Err != nil { | |
| 1005 | + | return templ_7745c5c3_Err | |
| 1006 | + | } | |
| 1007 | + | var templ_7745c5c3_Var56 string | |
| 1008 | + | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(a.Label) | |
| 1009 | + | if templ_7745c5c3_Err != nil { | |
| 1010 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 132, Col: 50} | |
| 1011 | + | } | |
| 1012 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | |
| 1013 | + | if templ_7745c5c3_Err != nil { | |
| 1014 | + | return templ_7745c5c3_Err | |
| 1015 | + | } | |
| 1016 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</a>") | |
| 1017 | + | if templ_7745c5c3_Err != nil { | |
| 1018 | + | return templ_7745c5c3_Err | |
| 1019 | + | } | |
| 1020 | + | } | |
| 1021 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</li>") | |
| 1022 | + | if templ_7745c5c3_Err != nil { | |
| 1023 | + | return templ_7745c5c3_Err | |
| 1024 | + | } | |
| 1025 | + | } | |
| 1026 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</ul>") | |
| 1027 | + | if templ_7745c5c3_Err != nil { | |
| 1028 | + | return templ_7745c5c3_Err | |
| 1029 | + | } | |
| 1030 | + | } | |
| 1031 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</section>") | |
| 1032 | + | if templ_7745c5c3_Err != nil { | |
| 1033 | + | return templ_7745c5c3_Err | |
| 1034 | + | } | |
| 1035 | + | if p.CancelAction != "" || p.RerunAction != "" { | |
| 1036 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<section aria-labelledby=\"r-act\" class=\"decide\"><h2 id=\"r-act\">Actions</h2><div class=\"d-btns\">") | |
| 1037 | + | if templ_7745c5c3_Err != nil { | |
| 1038 | + | return templ_7745c5c3_Err | |
| 1039 | + | } | |
| 1040 | + | if p.CancelAction != "" { | |
| 1041 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<form method=\"post\" action=\"") | |
| 1042 | + | if templ_7745c5c3_Err != nil { | |
| 1043 | + | return templ_7745c5c3_Err | |
| 1044 | + | } | |
| 1045 | + | var templ_7745c5c3_Var57 templ.SafeURL | |
| 1046 | + | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CancelAction)) | |
| 1047 | + | if templ_7745c5c3_Err != nil { | |
| 1048 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 144, Col: 63} | |
| 1049 | + | } | |
| 1050 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1051 | + | if templ_7745c5c3_Err != nil { | |
| 1052 | + | return templ_7745c5c3_Err | |
| 1053 | + | } | |
| 1054 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\"><button type=\"submit\" class=\"btn\">Cancel run</button></form>") | |
| 1055 | + | if templ_7745c5c3_Err != nil { | |
| 1056 | + | return templ_7745c5c3_Err | |
| 1057 | + | } | |
| 1058 | + | } | |
| 1059 | + | if p.RerunAction != "" { | |
| 1060 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<form method=\"post\" action=\"") | |
| 1061 | + | if templ_7745c5c3_Err != nil { | |
| 1062 | + | return templ_7745c5c3_Err | |
| 1063 | + | } | |
| 1064 | + | var templ_7745c5c3_Var58 templ.SafeURL | |
| 1065 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RerunAction)) | |
| 1066 | + | if templ_7745c5c3_Err != nil { | |
| 1067 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 147, Col: 62} | |
| 1068 | + | } | |
| 1069 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1070 | + | if templ_7745c5c3_Err != nil { | |
| 1071 | + | return templ_7745c5c3_Err | |
| 1072 | + | } | |
| 1073 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\"><button type=\"submit\" class=\"btn primary\">Re-run all jobs</button></form>") | |
| 1074 | + | if templ_7745c5c3_Err != nil { | |
| 1075 | + | return templ_7745c5c3_Err | |
| 1076 | + | } | |
| 1077 | + | } | |
| 1078 | + | if p.CanRerunFailed { | |
| 1079 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<form method=\"post\" action=\"") | |
| 1080 | + | if templ_7745c5c3_Err != nil { | |
| 1081 | + | return templ_7745c5c3_Err | |
| 1082 | + | } | |
| 1083 | + | var templ_7745c5c3_Var59 templ.SafeURL | |
| 1084 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RerunFailedAction)) | |
| 1085 | + | if templ_7745c5c3_Err != nil { | |
| 1086 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 150, Col: 68} | |
| 1087 | + | } | |
| 1088 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1089 | + | if templ_7745c5c3_Err != nil { | |
| 1090 | + | return templ_7745c5c3_Err | |
| 1091 | + | } | |
| 1092 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\"><button type=\"submit\" class=\"btn\">Re-run failed jobs</button></form>") | |
| 1093 | + | if templ_7745c5c3_Err != nil { | |
| 1094 | + | return templ_7745c5c3_Err | |
| 1095 | + | } | |
| 1096 | + | } | |
| 1097 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</div></section>") | |
| 1098 | + | if templ_7745c5c3_Err != nil { | |
| 1099 | + | return templ_7745c5c3_Err | |
| 1100 | + | } | |
| 1101 | + | } | |
| 1102 | + | return nil | |
| 1103 | + | }) | |
| 1104 | + | } | |
| 1105 | + | ||
| 1106 | + | // JobHead is the title of the job page with its state and its error, replaced | |
| 1107 | + | // by the live updates. total is the number of lines the log holds. | |
| 1108 | + | func JobHead(j JobRow, total int64) templ.Component { | |
| 1109 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1110 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1111 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1112 | + | return templ_7745c5c3_CtxErr | |
| 1113 | + | } | |
| 1114 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1115 | + | if !templ_7745c5c3_IsBuffer { | |
| 1116 | + | defer func() { | |
| 1117 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1118 | + | if templ_7745c5c3_Err == nil { | |
| 1119 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1120 | + | } | |
| 1121 | + | }() | |
| 1122 | + | } | |
| 1123 | + | ctx = templ.InitializeContext(ctx) | |
| 1124 | + | templ_7745c5c3_Var60 := templ.GetChildren(ctx) | |
| 1125 | + | if templ_7745c5c3_Var60 == nil { | |
| 1126 | + | templ_7745c5c3_Var60 = templ.NopComponent | |
| 1127 | + | } | |
| 1128 | + | ctx = templ.ClearChildren(ctx) | |
| 1129 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<div id=\"job-head\" data-total=\"") | |
| 1130 | + | if templ_7745c5c3_Err != nil { | |
| 1131 | + | return templ_7745c5c3_Err | |
| 1132 | + | } | |
| 1133 | + | var templ_7745c5c3_Var61 string | |
| 1134 | + | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(total)) | |
| 1135 | + | if templ_7745c5c3_Err != nil { | |
| 1136 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 160, Col: 46} | |
| 1137 | + | } | |
| 1138 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var61) | |
| 1139 | + | if templ_7745c5c3_Err != nil { | |
| 1140 | + | return templ_7745c5c3_Err | |
| 1141 | + | } | |
| 1142 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\"><h2 class=\"run-h\">") | |
| 1143 | + | if templ_7745c5c3_Err != nil { | |
| 1144 | + | return templ_7745c5c3_Err | |
| 1145 | + | } | |
| 1146 | + | templ_7745c5c3_Err = CheckIcon(j.Kind, j.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 1147 | + | if templ_7745c5c3_Err != nil { | |
| 1148 | + | return templ_7745c5c3_Err | |
| 1149 | + | } | |
| 1150 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<span>") | |
| 1151 | + | if templ_7745c5c3_Err != nil { | |
| 1152 | + | return templ_7745c5c3_Err | |
| 1153 | + | } | |
| 1154 | + | var templ_7745c5c3_Var62 string | |
| 1155 | + | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(j.Name) | |
| 1156 | + | if templ_7745c5c3_Err != nil { | |
| 1157 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 161, Col: 61} | |
| 1158 | + | } | |
| 1159 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1160 | + | if templ_7745c5c3_Err != nil { | |
| 1161 | + | return templ_7745c5c3_Err | |
| 1162 | + | } | |
| 1163 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</span> <span class=\"dim\">") | |
| 1164 | + | if templ_7745c5c3_Err != nil { | |
| 1165 | + | return templ_7745c5c3_Err | |
| 1166 | + | } | |
| 1167 | + | var templ_7745c5c3_Var63 string | |
| 1168 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(j.State) | |
| 1169 | + | if templ_7745c5c3_Err != nil { | |
| 1170 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 161, Col: 98} | |
| 1171 | + | } | |
| 1172 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | |
| 1173 | + | if templ_7745c5c3_Err != nil { | |
| 1174 | + | return templ_7745c5c3_Err | |
| 1175 | + | } | |
| 1176 | + | var templ_7745c5c3_Var64 string | |
| 1177 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1178 | + | if templ_7745c5c3_Err != nil { | |
| 1179 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 161, Col: 105} | |
| 1180 | + | } | |
| 1181 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1182 | + | if templ_7745c5c3_Err != nil { | |
| 1183 | + | return templ_7745c5c3_Err | |
| 1184 | + | } | |
| 1185 | + | var templ_7745c5c3_Var65 string | |
| 1186 | + | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(j.Duration) | |
| 1187 | + | if templ_7745c5c3_Err != nil { | |
| 1188 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 161, Col: 119} | |
| 1189 | + | } | |
| 1190 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1191 | + | if templ_7745c5c3_Err != nil { | |
| 1192 | + | return templ_7745c5c3_Err | |
| 1193 | + | } | |
| 1194 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</span></h2>") | |
| 1195 | + | if templ_7745c5c3_Err != nil { | |
| 1196 | + | return templ_7745c5c3_Err | |
| 1197 | + | } | |
| 1198 | + | if j.Error != "" { | |
| 1199 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<p class=\"job-err\">") | |
| 1200 | + | if templ_7745c5c3_Err != nil { | |
| 1201 | + | return templ_7745c5c3_Err | |
| 1202 | + | } | |
| 1203 | + | var templ_7745c5c3_Var66 string | |
| 1204 | + | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(j.Error) | |
| 1205 | + | if templ_7745c5c3_Err != nil { | |
| 1206 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 163, Col: 31} | |
| 1207 | + | } | |
| 1208 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | |
| 1209 | + | if templ_7745c5c3_Err != nil { | |
| 1210 | + | return templ_7745c5c3_Err | |
| 1211 | + | } | |
| 1212 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</p>") | |
| 1213 | + | if templ_7745c5c3_Err != nil { | |
| 1214 | + | return templ_7745c5c3_Err | |
| 1215 | + | } | |
| 1216 | + | } | |
| 1217 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "</div>") | |
| 1218 | + | if templ_7745c5c3_Err != nil { | |
| 1219 | + | return templ_7745c5c3_Err | |
| 1220 | + | } | |
| 1221 | + | return nil | |
| 1222 | + | }) | |
| 1223 | + | } | |
| 1224 | + | ||
| 1225 | + | // JobSteps is the steps of the job in the rail, replaced by the live updates. | |
| 1226 | + | func JobSteps(steps []StepView) templ.Component { | |
| 1227 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1228 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1229 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1230 | + | return templ_7745c5c3_CtxErr | |
| 1231 | + | } | |
| 1232 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1233 | + | if !templ_7745c5c3_IsBuffer { | |
| 1234 | + | defer func() { | |
| 1235 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1236 | + | if templ_7745c5c3_Err == nil { | |
| 1237 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1238 | + | } | |
| 1239 | + | }() | |
| 1240 | + | } | |
| 1241 | + | ctx = templ.InitializeContext(ctx) | |
| 1242 | + | templ_7745c5c3_Var67 := templ.GetChildren(ctx) | |
| 1243 | + | if templ_7745c5c3_Var67 == nil { | |
| 1244 | + | templ_7745c5c3_Var67 = templ.NopComponent | |
| 1245 | + | } | |
| 1246 | + | ctx = templ.ClearChildren(ctx) | |
| 1247 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<ol class=\"steps\" id=\"job-steps\">") | |
| 1248 | + | if templ_7745c5c3_Err != nil { | |
| 1249 | + | return templ_7745c5c3_Err | |
| 1250 | + | } | |
| 1251 | + | for _, s := range steps { | |
| 1252 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<li>") | |
| 1253 | + | if templ_7745c5c3_Err != nil { | |
| 1254 | + | return templ_7745c5c3_Err | |
| 1255 | + | } | |
| 1256 | + | templ_7745c5c3_Err = CheckIcon(s.Kind, s.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 1257 | + | if templ_7745c5c3_Err != nil { | |
| 1258 | + | return templ_7745c5c3_Err | |
| 1259 | + | } | |
| 1260 | + | if s.LogEnd > s.LogStart { | |
| 1261 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<a href=\"") | |
| 1262 | + | if templ_7745c5c3_Err != nil { | |
| 1263 | + | return templ_7745c5c3_Err | |
| 1264 | + | } | |
| 1265 | + | var templ_7745c5c3_Var68 templ.SafeURL | |
| 1266 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#L" + itoa64(s.LogStart))) | |
| 1267 | + | if templ_7745c5c3_Err != nil { | |
| 1268 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 175, Col: 55} | |
| 1269 | + | } | |
| 1270 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1271 | + | if templ_7745c5c3_Err != nil { | |
| 1272 | + | return templ_7745c5c3_Err | |
| 1273 | + | } | |
| 1274 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\">") | |
| 1275 | + | if templ_7745c5c3_Err != nil { | |
| 1276 | + | return templ_7745c5c3_Err | |
| 1277 | + | } | |
| 1278 | + | var templ_7745c5c3_Var69 string | |
| 1279 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name) | |
| 1280 | + | if templ_7745c5c3_Err != nil { | |
| 1281 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 175, Col: 66} | |
| 1282 | + | } | |
| 1283 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1284 | + | if templ_7745c5c3_Err != nil { | |
| 1285 | + | return templ_7745c5c3_Err | |
| 1286 | + | } | |
| 1287 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</a> ") | |
| 1288 | + | if templ_7745c5c3_Err != nil { | |
| 1289 | + | return templ_7745c5c3_Err | |
| 1290 | + | } | |
| 1291 | + | } else { | |
| 1292 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "<span>") | |
| 1293 | + | if templ_7745c5c3_Err != nil { | |
| 1294 | + | return templ_7745c5c3_Err | |
| 1295 | + | } | |
| 1296 | + | var templ_7745c5c3_Var70 string | |
| 1297 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name) | |
| 1298 | + | if templ_7745c5c3_Err != nil { | |
| 1299 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 177, Col: 19} | |
| 1300 | + | } | |
| 1301 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1302 | + | if templ_7745c5c3_Err != nil { | |
| 1303 | + | return templ_7745c5c3_Err | |
| 1304 | + | } | |
| 1305 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</span> ") | |
| 1306 | + | if templ_7745c5c3_Err != nil { | |
| 1307 | + | return templ_7745c5c3_Err | |
| 1308 | + | } | |
| 1309 | + | } | |
| 1310 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<span class=\"dim\">") | |
| 1311 | + | if templ_7745c5c3_Err != nil { | |
| 1312 | + | return templ_7745c5c3_Err | |
| 1313 | + | } | |
| 1314 | + | var templ_7745c5c3_Var71 string | |
| 1315 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(s.Duration) | |
| 1316 | + | if templ_7745c5c3_Err != nil { | |
| 1317 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 179, Col: 34} | |
| 1318 | + | } | |
| 1319 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1320 | + | if templ_7745c5c3_Err != nil { | |
| 1321 | + | return templ_7745c5c3_Err | |
| 1322 | + | } | |
| 1323 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</span></li>") | |
| 1324 | + | if templ_7745c5c3_Err != nil { | |
| 1325 | + | return templ_7745c5c3_Err | |
| 1326 | + | } | |
| 1327 | + | } | |
| 1328 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "</ol>") | |
| 1329 | + | if templ_7745c5c3_Err != nil { | |
| 1330 | + | return templ_7745c5c3_Err | |
| 1331 | + | } | |
| 1332 | + | return nil | |
| 1333 | + | }) | |
| 1334 | + | } | |
| 1335 | + | ||
| 1336 | + | func Job(l Layout, p JobPage) templ.Component { | |
| 1337 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1338 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1339 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1340 | + | return templ_7745c5c3_CtxErr | |
| 1341 | + | } | |
| 1342 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1343 | + | if !templ_7745c5c3_IsBuffer { | |
| 1344 | + | defer func() { | |
| 1345 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1346 | + | if templ_7745c5c3_Err == nil { | |
| 1347 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1348 | + | } | |
| 1349 | + | }() | |
| 1350 | + | } | |
| 1351 | + | ctx = templ.InitializeContext(ctx) | |
| 1352 | + | templ_7745c5c3_Var72 := templ.GetChildren(ctx) | |
| 1353 | + | if templ_7745c5c3_Var72 == nil { | |
| 1354 | + | templ_7745c5c3_Var72 = templ.NopComponent | |
| 1355 | + | } | |
| 1356 | + | ctx = templ.ClearChildren(ctx) | |
| 1357 | + | templ_7745c5c3_Var73 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1358 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1359 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1360 | + | if !templ_7745c5c3_IsBuffer { | |
| 1361 | + | defer func() { | |
| 1362 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1363 | + | if templ_7745c5c3_Err == nil { | |
| 1364 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1365 | + | } | |
| 1366 | + | }() | |
| 1367 | + | } | |
| 1368 | + | ctx = templ.InitializeContext(ctx) | |
| 1369 | + | templ_7745c5c3_Err = LiveSlot("job-notice").Render(ctx, templ_7745c5c3_Buffer) | |
| 1370 | + | if templ_7745c5c3_Err != nil { | |
| 1371 | + | return templ_7745c5c3_Err | |
| 1372 | + | } | |
| 1373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " ") | |
| 1374 | + | if templ_7745c5c3_Err != nil { | |
| 1375 | + | return templ_7745c5c3_Err | |
| 1376 | + | } | |
| 1377 | + | templ_7745c5c3_Err = JobHead(p.JobRow, p.Total).Render(ctx, templ_7745c5c3_Buffer) | |
| 1378 | + | if templ_7745c5c3_Err != nil { | |
| 1379 | + | return templ_7745c5c3_Err | |
| 1380 | + | } | |
| 1381 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, " <div id=\"job-log\" class=\"joblog\" data-total=\"") | |
| 1382 | + | if templ_7745c5c3_Err != nil { | |
| 1383 | + | return templ_7745c5c3_Err | |
| 1384 | + | } | |
| 1385 | + | var templ_7745c5c3_Var74 string | |
| 1386 | + | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.Total)) | |
| 1387 | + | if templ_7745c5c3_Err != nil { | |
| 1388 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 189, Col: 63} | |
| 1389 | + | } | |
| 1390 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1391 | + | if templ_7745c5c3_Err != nil { | |
| 1392 | + | return templ_7745c5c3_Err | |
| 1393 | + | } | |
| 1394 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\"") | |
| 1395 | + | if templ_7745c5c3_Err != nil { | |
| 1396 | + | return templ_7745c5c3_Err | |
| 1397 | + | } | |
| 1398 | + | if p.Follow { | |
| 1399 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, " data-follow") | |
| 1400 | + | if templ_7745c5c3_Err != nil { | |
| 1401 | + | return templ_7745c5c3_Err | |
| 1402 | + | } | |
| 1403 | + | } | |
| 1404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " data-stream=\"") | |
| 1405 | + | if templ_7745c5c3_Err != nil { | |
| 1406 | + | return templ_7745c5c3_Err | |
| 1407 | + | } | |
| 1408 | + | var templ_7745c5c3_Var75 string | |
| 1409 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.StreamHref) | |
| 1410 | + | if templ_7745c5c3_Err != nil { | |
| 1411 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 189, Col: 118} | |
| 1412 | + | } | |
| 1413 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75) | |
| 1414 | + | if templ_7745c5c3_Err != nil { | |
| 1415 | + | return templ_7745c5c3_Err | |
| 1416 | + | } | |
| 1417 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\" data-next=\"") | |
| 1418 | + | if templ_7745c5c3_Err != nil { | |
| 1419 | + | return templ_7745c5c3_Err | |
| 1420 | + | } | |
| 1421 | + | var templ_7745c5c3_Var76 string | |
| 1422 | + | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.From + int64(len(p.Lines)))) | |
| 1423 | + | if templ_7745c5c3_Err != nil { | |
| 1424 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 189, Col: 169} | |
| 1425 | + | } | |
| 1426 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76) | |
| 1427 | + | if templ_7745c5c3_Err != nil { | |
| 1428 | + | return templ_7745c5c3_Err | |
| 1429 | + | } | |
| 1430 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\">") | |
| 1431 | + | if templ_7745c5c3_Err != nil { | |
| 1432 | + | return templ_7745c5c3_Err | |
| 1433 | + | } | |
| 1434 | + | if p.Note != "" { | |
| 1435 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"dim\">") | |
| 1436 | + | if templ_7745c5c3_Err != nil { | |
| 1437 | + | return templ_7745c5c3_Err | |
| 1438 | + | } | |
| 1439 | + | var templ_7745c5c3_Var77 string | |
| 1440 | + | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 1441 | + | if templ_7745c5c3_Err != nil { | |
| 1442 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 191, Col: 27} | |
| 1443 | + | } | |
| 1444 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) | |
| 1445 | + | if templ_7745c5c3_Err != nil { | |
| 1446 | + | return templ_7745c5c3_Err | |
| 1447 | + | } | |
| 1448 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</p>") | |
| 1449 | + | if templ_7745c5c3_Err != nil { | |
| 1450 | + | return templ_7745c5c3_Err | |
| 1451 | + | } | |
| 1452 | + | } | |
| 1453 | + | if p.Prev != "" { | |
| 1454 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<p class=\"more\"><a href=\"") | |
| 1455 | + | if templ_7745c5c3_Err != nil { | |
| 1456 | + | return templ_7745c5c3_Err | |
| 1457 | + | } | |
| 1458 | + | var templ_7745c5c3_Var78 templ.SafeURL | |
| 1459 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Prev)) | |
| 1460 | + | if templ_7745c5c3_Err != nil { | |
| 1461 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 194, Col: 51} | |
| 1462 | + | } | |
| 1463 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | |
| 1464 | + | if templ_7745c5c3_Err != nil { | |
| 1465 | + | return templ_7745c5c3_Err | |
| 1466 | + | } | |
| 1467 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" rel=\"prev\">earlier lines</a></p>") | |
| 1468 | + | if templ_7745c5c3_Err != nil { | |
| 1469 | + | return templ_7745c5c3_Err | |
| 1470 | + | } | |
| 1471 | + | } | |
| 1472 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<ol class=\"log\" start=\"") | |
| 1473 | + | if templ_7745c5c3_Err != nil { | |
| 1474 | + | return templ_7745c5c3_Err | |
| 1475 | + | } | |
| 1476 | + | var templ_7745c5c3_Var79 string | |
| 1477 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.From + 1)) | |
| 1478 | + | if templ_7745c5c3_Err != nil { | |
| 1479 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 196, Col: 45} | |
| 1480 | + | } | |
| 1481 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | |
| 1482 | + | if templ_7745c5c3_Err != nil { | |
| 1483 | + | return templ_7745c5c3_Err | |
| 1484 | + | } | |
| 1485 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\">") | |
| 1486 | + | if templ_7745c5c3_Err != nil { | |
| 1487 | + | return templ_7745c5c3_Err | |
| 1488 | + | } | |
| 1489 | + | for _, ln := range p.Lines { | |
| 1490 | + | templ_7745c5c3_Err = LogLine(ln).Render(ctx, templ_7745c5c3_Buffer) | |
| 1491 | + | if templ_7745c5c3_Err != nil { | |
| 1492 | + | return templ_7745c5c3_Err | |
| 1493 | + | } | |
| 1494 | + | } | |
| 1495 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</ol>") | |
| 1496 | + | if templ_7745c5c3_Err != nil { | |
| 1497 | + | return templ_7745c5c3_Err | |
| 1498 | + | } | |
| 1499 | + | if p.Next != "" { | |
| 1500 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<p class=\"more\"><a href=\"") | |
| 1501 | + | if templ_7745c5c3_Err != nil { | |
| 1502 | + | return templ_7745c5c3_Err | |
| 1503 | + | } | |
| 1504 | + | var templ_7745c5c3_Var80 templ.SafeURL | |
| 1505 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Next)) | |
| 1506 | + | if templ_7745c5c3_Err != nil { | |
| 1507 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 202, Col: 51} | |
| 1508 | + | } | |
| 1509 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) | |
| 1510 | + | if templ_7745c5c3_Err != nil { | |
| 1511 | + | return templ_7745c5c3_Err | |
| 1512 | + | } | |
| 1513 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\" rel=\"next\">more lines</a></p>") | |
| 1514 | + | if templ_7745c5c3_Err != nil { | |
| 1515 | + | return templ_7745c5c3_Err | |
| 1516 | + | } | |
| 1517 | + | } | |
| 1518 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</div>") | |
| 1519 | + | if templ_7745c5c3_Err != nil { | |
| 1520 | + | return templ_7745c5c3_Err | |
| 1521 | + | } | |
| 1522 | + | return nil | |
| 1523 | + | }) | |
| 1524 | + | templ_7745c5c3_Err = Shell(l, jobRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var73), templ_7745c5c3_Buffer) | |
| 1525 | + | if templ_7745c5c3_Err != nil { | |
| 1526 | + | return templ_7745c5c3_Err | |
| 1527 | + | } | |
| 1528 | + | return nil | |
| 1529 | + | }) | |
| 1530 | + | } | |
| 1531 | + | ||
| 1532 | + | // LogLine is a line of the log. Group and error markers of the runner show as | |
| 1533 | + | // headings and errors, never as raw text. | |
| 1534 | + | func LogLine(ln LogLineView) templ.Component { | |
| 1535 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1536 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1537 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1538 | + | return templ_7745c5c3_CtxErr | |
| 1539 | + | } | |
| 1540 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1541 | + | if !templ_7745c5c3_IsBuffer { | |
| 1542 | + | defer func() { | |
| 1543 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1544 | + | if templ_7745c5c3_Err == nil { | |
| 1545 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1546 | + | } | |
| 1547 | + | }() | |
| 1548 | + | } | |
| 1549 | + | ctx = templ.InitializeContext(ctx) | |
| 1550 | + | templ_7745c5c3_Var81 := templ.GetChildren(ctx) | |
| 1551 | + | if templ_7745c5c3_Var81 == nil { | |
| 1552 | + | templ_7745c5c3_Var81 = templ.NopComponent | |
| 1553 | + | } | |
| 1554 | + | ctx = templ.ClearChildren(ctx) | |
| 1555 | + | var templ_7745c5c3_Var82 = []any{logClass(ln)} | |
| 1556 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var82...) | |
| 1557 | + | if templ_7745c5c3_Err != nil { | |
| 1558 | + | return templ_7745c5c3_Err | |
| 1559 | + | } | |
| 1560 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<li id=\"") | |
| 1561 | + | if templ_7745c5c3_Err != nil { | |
| 1562 | + | return templ_7745c5c3_Err | |
| 1563 | + | } | |
| 1564 | + | var templ_7745c5c3_Var83 string | |
| 1565 | + | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue("L" + itoa64(ln.N)) | |
| 1566 | + | if templ_7745c5c3_Err != nil { | |
| 1567 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 211, Col: 28} | |
| 1568 | + | } | |
| 1569 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | |
| 1570 | + | if templ_7745c5c3_Err != nil { | |
| 1571 | + | return templ_7745c5c3_Err | |
| 1572 | + | } | |
| 1573 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" class=\"") | |
| 1574 | + | if templ_7745c5c3_Err != nil { | |
| 1575 | + | return templ_7745c5c3_Err | |
| 1576 | + | } | |
| 1577 | + | var templ_7745c5c3_Var84 string | |
| 1578 | + | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var82).String()) | |
| 1579 | + | if templ_7745c5c3_Err != nil { | |
| 1580 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 1, Col: 0} | |
| 1581 | + | } | |
| 1582 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) | |
| 1583 | + | if templ_7745c5c3_Err != nil { | |
| 1584 | + | return templ_7745c5c3_Err | |
| 1585 | + | } | |
| 1586 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\" data-step=\"") | |
| 1587 | + | if templ_7745c5c3_Err != nil { | |
| 1588 | + | return templ_7745c5c3_Err | |
| 1589 | + | } | |
| 1590 | + | var templ_7745c5c3_Var85 string | |
| 1591 | + | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(int(ln.Step))) | |
| 1592 | + | if templ_7745c5c3_Err != nil { | |
| 1593 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 211, Col: 84} | |
| 1594 | + | } | |
| 1595 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var85) | |
| 1596 | + | if templ_7745c5c3_Err != nil { | |
| 1597 | + | return templ_7745c5c3_Err | |
| 1598 | + | } | |
| 1599 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\"><time>") | |
| 1600 | + | if templ_7745c5c3_Err != nil { | |
| 1601 | + | return templ_7745c5c3_Err | |
| 1602 | + | } | |
| 1603 | + | var templ_7745c5c3_Var86 string | |
| 1604 | + | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Time) | |
| 1605 | + | if templ_7745c5c3_Err != nil { | |
| 1606 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 211, Col: 102} | |
| 1607 | + | } | |
| 1608 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86)) | |
| 1609 | + | if templ_7745c5c3_Err != nil { | |
| 1610 | + | return templ_7745c5c3_Err | |
| 1611 | + | } | |
| 1612 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</time><span class=\"t\">") | |
| 1613 | + | if templ_7745c5c3_Err != nil { | |
| 1614 | + | return templ_7745c5c3_Err | |
| 1615 | + | } | |
| 1616 | + | var templ_7745c5c3_Var87 string | |
| 1617 | + | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Text) | |
| 1618 | + | if templ_7745c5c3_Err != nil { | |
| 1619 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 211, Col: 136} | |
| 1620 | + | } | |
| 1621 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) | |
| 1622 | + | if templ_7745c5c3_Err != nil { | |
| 1623 | + | return templ_7745c5c3_Err | |
| 1624 | + | } | |
| 1625 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</span></li>") | |
| 1626 | + | if templ_7745c5c3_Err != nil { | |
| 1627 | + | return templ_7745c5c3_Err | |
| 1628 | + | } | |
| 1629 | + | return nil | |
| 1630 | + | }) | |
| 1631 | + | } | |
| 1632 | + | ||
| 1633 | + | func jobRail(p JobPage) templ.Component { | |
| 1634 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1635 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1636 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1637 | + | return templ_7745c5c3_CtxErr | |
| 1638 | + | } | |
| 1639 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1640 | + | if !templ_7745c5c3_IsBuffer { | |
| 1641 | + | defer func() { | |
| 1642 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1643 | + | if templ_7745c5c3_Err == nil { | |
| 1644 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1645 | + | } | |
| 1646 | + | }() | |
| 1647 | + | } | |
| 1648 | + | ctx = templ.InitializeContext(ctx) | |
| 1649 | + | templ_7745c5c3_Var88 := templ.GetChildren(ctx) | |
| 1650 | + | if templ_7745c5c3_Var88 == nil { | |
| 1651 | + | templ_7745c5c3_Var88 = templ.NopComponent | |
| 1652 | + | } | |
| 1653 | + | ctx = templ.ClearChildren(ctx) | |
| 1654 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<section aria-labelledby=\"r-job\"><h2 id=\"r-job\">Steps</h2>") | |
| 1655 | + | if templ_7745c5c3_Err != nil { | |
| 1656 | + | return templ_7745c5c3_Err | |
| 1657 | + | } | |
| 1658 | + | templ_7745c5c3_Err = JobSteps(p.Steps).Render(ctx, templ_7745c5c3_Buffer) | |
| 1659 | + | if templ_7745c5c3_Err != nil { | |
| 1660 | + | return templ_7745c5c3_Err | |
| 1661 | + | } | |
| 1662 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</section><section aria-labelledby=\"r-jobf\"><h2 id=\"r-jobf\">Job</h2><dl class=\"kv\"><dt>run</dt><dd><a href=\"") | |
| 1663 | + | if templ_7745c5c3_Err != nil { | |
| 1664 | + | return templ_7745c5c3_Err | |
| 1665 | + | } | |
| 1666 | + | var templ_7745c5c3_Var89 templ.SafeURL | |
| 1667 | + | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RunHref)) | |
| 1668 | + | if templ_7745c5c3_Err != nil { | |
| 1669 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 223, Col: 41} | |
| 1670 | + | } | |
| 1671 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1672 | + | if templ_7745c5c3_Err != nil { | |
| 1673 | + | return templ_7745c5c3_Err | |
| 1674 | + | } | |
| 1675 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "\">") | |
| 1676 | + | if templ_7745c5c3_Err != nil { | |
| 1677 | + | return templ_7745c5c3_Err | |
| 1678 | + | } | |
| 1679 | + | var templ_7745c5c3_Var90 string | |
| 1680 | + | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(p.RunName) | |
| 1681 | + | if templ_7745c5c3_Err != nil { | |
| 1682 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 223, Col: 55} | |
| 1683 | + | } | |
| 1684 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90)) | |
| 1685 | + | if templ_7745c5c3_Err != nil { | |
| 1686 | + | return templ_7745c5c3_Err | |
| 1687 | + | } | |
| 1688 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</a></dd>") | |
| 1689 | + | if templ_7745c5c3_Err != nil { | |
| 1690 | + | return templ_7745c5c3_Err | |
| 1691 | + | } | |
| 1692 | + | if p.Runner != "" { | |
| 1693 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<dt>runner</dt><dd>") | |
| 1694 | + | if templ_7745c5c3_Err != nil { | |
| 1695 | + | return templ_7745c5c3_Err | |
| 1696 | + | } | |
| 1697 | + | var templ_7745c5c3_Var91 string | |
| 1698 | + | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(p.Runner) | |
| 1699 | + | if templ_7745c5c3_Err != nil { | |
| 1700 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 226, Col: 18} | |
| 1701 | + | } | |
| 1702 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91)) | |
| 1703 | + | if templ_7745c5c3_Err != nil { | |
| 1704 | + | return templ_7745c5c3_Err | |
| 1705 | + | } | |
| 1706 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "</dd>") | |
| 1707 | + | if templ_7745c5c3_Err != nil { | |
| 1708 | + | return templ_7745c5c3_Err | |
| 1709 | + | } | |
| 1710 | + | } | |
| 1711 | + | if p.Isolation != "" { | |
| 1712 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "<dt>isolation</dt><dd>") | |
| 1713 | + | if templ_7745c5c3_Err != nil { | |
| 1714 | + | return templ_7745c5c3_Err | |
| 1715 | + | } | |
| 1716 | + | var templ_7745c5c3_Var92 string | |
| 1717 | + | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(p.Isolation) | |
| 1718 | + | if templ_7745c5c3_Err != nil { | |
| 1719 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 230, Col: 21} | |
| 1720 | + | } | |
| 1721 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92)) | |
| 1722 | + | if templ_7745c5c3_Err != nil { | |
| 1723 | + | return templ_7745c5c3_Err | |
| 1724 | + | } | |
| 1725 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</dd>") | |
| 1726 | + | if templ_7745c5c3_Err != nil { | |
| 1727 | + | return templ_7745c5c3_Err | |
| 1728 | + | } | |
| 1729 | + | } | |
| 1730 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<dt>lines</dt><dd>") | |
| 1731 | + | if templ_7745c5c3_Err != nil { | |
| 1732 | + | return templ_7745c5c3_Err | |
| 1733 | + | } | |
| 1734 | + | var templ_7745c5c3_Var93 string | |
| 1735 | + | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinStringErrs(itoa64(p.Total)) | |
| 1736 | + | if templ_7745c5c3_Err != nil { | |
| 1737 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 233, Col: 24} | |
| 1738 | + | } | |
| 1739 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93)) | |
| 1740 | + | if templ_7745c5c3_Err != nil { | |
| 1741 | + | return templ_7745c5c3_Err | |
| 1742 | + | } | |
| 1743 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</dd></dl><p><a href=\"") | |
| 1744 | + | if templ_7745c5c3_Err != nil { | |
| 1745 | + | return templ_7745c5c3_Err | |
| 1746 | + | } | |
| 1747 | + | var templ_7745c5c3_Var94 templ.SafeURL | |
| 1748 | + | templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RawHref)) | |
| 1749 | + | if templ_7745c5c3_Err != nil { | |
| 1750 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 235, Col: 39} | |
| 1751 | + | } | |
| 1752 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94)) | |
| 1753 | + | if templ_7745c5c3_Err != nil { | |
| 1754 | + | return templ_7745c5c3_Err | |
| 1755 | + | } | |
| 1756 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\">raw log</a></p></section>") | |
| 1757 | + | if templ_7745c5c3_Err != nil { | |
| 1758 | + | return templ_7745c5c3_Err | |
| 1759 | + | } | |
| 1760 | + | return nil | |
| 1761 | + | }) | |
| 1762 | + | } | |
| 1763 | + | ||
| 1764 | + | // CommitChecks is the combined state of the checks of a commit, as a mark | |
| 1765 | + | // with a count, linking to the page that explains it. | |
| 1766 | + | func CommitChecks(c ChecksSummary) templ.Component { | |
| 1767 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1768 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1769 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1770 | + | return templ_7745c5c3_CtxErr | |
| 1771 | + | } | |
| 1772 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1773 | + | if !templ_7745c5c3_IsBuffer { | |
| 1774 | + | defer func() { | |
| 1775 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1776 | + | if templ_7745c5c3_Err == nil { | |
| 1777 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1778 | + | } | |
| 1779 | + | }() | |
| 1780 | + | } | |
| 1781 | + | ctx = templ.InitializeContext(ctx) | |
| 1782 | + | templ_7745c5c3_Var95 := templ.GetChildren(ctx) | |
| 1783 | + | if templ_7745c5c3_Var95 == nil { | |
| 1784 | + | templ_7745c5c3_Var95 = templ.NopComponent | |
| 1785 | + | } | |
| 1786 | + | ctx = templ.ClearChildren(ctx) | |
| 1787 | + | if c.Total > 0 { | |
| 1788 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<a class=\"ck-sum\" href=\"") | |
| 1789 | + | if templ_7745c5c3_Err != nil { | |
| 1790 | + | return templ_7745c5c3_Err | |
| 1791 | + | } | |
| 1792 | + | var templ_7745c5c3_Var96 templ.SafeURL | |
| 1793 | + | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 1794 | + | if templ_7745c5c3_Err != nil { | |
| 1795 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 243, Col: 48} | |
| 1796 | + | } | |
| 1797 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | |
| 1798 | + | if templ_7745c5c3_Err != nil { | |
| 1799 | + | return templ_7745c5c3_Err | |
| 1800 | + | } | |
| 1801 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\" title=\"") | |
| 1802 | + | if templ_7745c5c3_Err != nil { | |
| 1803 | + | return templ_7745c5c3_Err | |
| 1804 | + | } | |
| 1805 | + | var templ_7745c5c3_Var97 string | |
| 1806 | + | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Title) | |
| 1807 | + | if templ_7745c5c3_Err != nil { | |
| 1808 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 243, Col: 66} | |
| 1809 | + | } | |
| 1810 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var97) | |
| 1811 | + | if templ_7745c5c3_Err != nil { | |
| 1812 | + | return templ_7745c5c3_Err | |
| 1813 | + | } | |
| 1814 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "\">") | |
| 1815 | + | if templ_7745c5c3_Err != nil { | |
| 1816 | + | return templ_7745c5c3_Err | |
| 1817 | + | } | |
| 1818 | + | templ_7745c5c3_Err = CheckIcon(c.Kind, c.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 1819 | + | if templ_7745c5c3_Err != nil { | |
| 1820 | + | return templ_7745c5c3_Err | |
| 1821 | + | } | |
| 1822 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<span class=\"mute\">") | |
| 1823 | + | if templ_7745c5c3_Err != nil { | |
| 1824 | + | return templ_7745c5c3_Err | |
| 1825 | + | } | |
| 1826 | + | var templ_7745c5c3_Var98 string | |
| 1827 | + | templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 1828 | + | if templ_7745c5c3_Err != nil { | |
| 1829 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 245, Col: 31} | |
| 1830 | + | } | |
| 1831 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98)) | |
| 1832 | + | if templ_7745c5c3_Err != nil { | |
| 1833 | + | return templ_7745c5c3_Err | |
| 1834 | + | } | |
| 1835 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "</span></a>") | |
| 1836 | + | if templ_7745c5c3_Err != nil { | |
| 1837 | + | return templ_7745c5c3_Err | |
| 1838 | + | } | |
| 1839 | + | } | |
| 1840 | + | return nil | |
| 1841 | + | }) | |
| 1842 | + | } | |
| 1843 | + | ||
| 1844 | + | // MRChecks is the checks section of the rail of a merge request. | |
| 1845 | + | func MRChecks(c ChecksSection) templ.Component { | |
| 1846 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1847 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1848 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1849 | + | return templ_7745c5c3_CtxErr | |
| 1850 | + | } | |
| 1851 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1852 | + | if !templ_7745c5c3_IsBuffer { | |
| 1853 | + | defer func() { | |
| 1854 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1855 | + | if templ_7745c5c3_Err == nil { | |
| 1856 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1857 | + | } | |
| 1858 | + | }() | |
| 1859 | + | } | |
| 1860 | + | ctx = templ.InitializeContext(ctx) | |
| 1861 | + | templ_7745c5c3_Var99 := templ.GetChildren(ctx) | |
| 1862 | + | if templ_7745c5c3_Var99 == nil { | |
| 1863 | + | templ_7745c5c3_Var99 = templ.NopComponent | |
| 1864 | + | } | |
| 1865 | + | ctx = templ.ClearChildren(ctx) | |
| 1866 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<section aria-labelledby=\"r-checks\" id=\"mr-checks\"><h2 id=\"r-checks\">Checks <span class=\"count\">") | |
| 1867 | + | if templ_7745c5c3_Err != nil { | |
| 1868 | + | return templ_7745c5c3_Err | |
| 1869 | + | } | |
| 1870 | + | var templ_7745c5c3_Var100 string | |
| 1871 | + | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(c.Rows))) | |
| 1872 | + | if templ_7745c5c3_Err != nil { | |
| 1873 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 253, Col: 66} | |
| 1874 | + | } | |
| 1875 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 1876 | + | if templ_7745c5c3_Err != nil { | |
| 1877 | + | return templ_7745c5c3_Err | |
| 1878 | + | } | |
| 1879 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "</span></h2>") | |
| 1880 | + | if templ_7745c5c3_Err != nil { | |
| 1881 | + | return templ_7745c5c3_Err | |
| 1882 | + | } | |
| 1883 | + | if len(c.Rows) == 0 { | |
| 1884 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<p class=\"dim\">no checks</p>") | |
| 1885 | + | if templ_7745c5c3_Err != nil { | |
| 1886 | + | return templ_7745c5c3_Err | |
| 1887 | + | } | |
| 1888 | + | } else { | |
| 1889 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "<ul class=\"checks\">") | |
| 1890 | + | if templ_7745c5c3_Err != nil { | |
| 1891 | + | return templ_7745c5c3_Err | |
| 1892 | + | } | |
| 1893 | + | for _, r := range c.Rows { | |
| 1894 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<li>") | |
| 1895 | + | if templ_7745c5c3_Err != nil { | |
| 1896 | + | return templ_7745c5c3_Err | |
| 1897 | + | } | |
| 1898 | + | templ_7745c5c3_Err = CheckIcon(r.Kind, r.State).Render(ctx, templ_7745c5c3_Buffer) | |
| 1899 | + | if templ_7745c5c3_Err != nil { | |
| 1900 | + | return templ_7745c5c3_Err | |
| 1901 | + | } | |
| 1902 | + | if r.Href != "" { | |
| 1903 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<a href=\"") | |
| 1904 | + | if templ_7745c5c3_Err != nil { | |
| 1905 | + | return templ_7745c5c3_Err | |
| 1906 | + | } | |
| 1907 | + | var templ_7745c5c3_Var101 templ.SafeURL | |
| 1908 | + | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 1909 | + | if templ_7745c5c3_Err != nil { | |
| 1910 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 262, Col: 38} | |
| 1911 | + | } | |
| 1912 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | |
| 1913 | + | if templ_7745c5c3_Err != nil { | |
| 1914 | + | return templ_7745c5c3_Err | |
| 1915 | + | } | |
| 1916 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "\">") | |
| 1917 | + | if templ_7745c5c3_Err != nil { | |
| 1918 | + | return templ_7745c5c3_Err | |
| 1919 | + | } | |
| 1920 | + | var templ_7745c5c3_Var102 string | |
| 1921 | + | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 1922 | + | if templ_7745c5c3_Err != nil { | |
| 1923 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 262, Col: 49} | |
| 1924 | + | } | |
| 1925 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) | |
| 1926 | + | if templ_7745c5c3_Err != nil { | |
| 1927 | + | return templ_7745c5c3_Err | |
| 1928 | + | } | |
| 1929 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "</a> ") | |
| 1930 | + | if templ_7745c5c3_Err != nil { | |
| 1931 | + | return templ_7745c5c3_Err | |
| 1932 | + | } | |
| 1933 | + | } else { | |
| 1934 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "<span>") | |
| 1935 | + | if templ_7745c5c3_Err != nil { | |
| 1936 | + | return templ_7745c5c3_Err | |
| 1937 | + | } | |
| 1938 | + | var templ_7745c5c3_Var103 string | |
| 1939 | + | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 1940 | + | if templ_7745c5c3_Err != nil { | |
| 1941 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 264, Col: 21} | |
| 1942 | + | } | |
| 1943 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) | |
| 1944 | + | if templ_7745c5c3_Err != nil { | |
| 1945 | + | return templ_7745c5c3_Err | |
| 1946 | + | } | |
| 1947 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "</span> ") | |
| 1948 | + | if templ_7745c5c3_Err != nil { | |
| 1949 | + | return templ_7745c5c3_Err | |
| 1950 | + | } | |
| 1951 | + | } | |
| 1952 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "<span class=\"dim\">") | |
| 1953 | + | if templ_7745c5c3_Err != nil { | |
| 1954 | + | return templ_7745c5c3_Err | |
| 1955 | + | } | |
| 1956 | + | var templ_7745c5c3_Var104 string | |
| 1957 | + | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(r.State) | |
| 1958 | + | if templ_7745c5c3_Err != nil { | |
| 1959 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 266, Col: 33} | |
| 1960 | + | } | |
| 1961 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | |
| 1962 | + | if templ_7745c5c3_Err != nil { | |
| 1963 | + | return templ_7745c5c3_Err | |
| 1964 | + | } | |
| 1965 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</span> ") | |
| 1966 | + | if templ_7745c5c3_Err != nil { | |
| 1967 | + | return templ_7745c5c3_Err | |
| 1968 | + | } | |
| 1969 | + | if r.Waiting { | |
| 1970 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<span class=\"mute\">waiting for a runner</span>") | |
| 1971 | + | if templ_7745c5c3_Err != nil { | |
| 1972 | + | return templ_7745c5c3_Err | |
| 1973 | + | } | |
| 1974 | + | } | |
| 1975 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "</li>") | |
| 1976 | + | if templ_7745c5c3_Err != nil { | |
| 1977 | + | return templ_7745c5c3_Err | |
| 1978 | + | } | |
| 1979 | + | } | |
| 1980 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</ul>") | |
| 1981 | + | if templ_7745c5c3_Err != nil { | |
| 1982 | + | return templ_7745c5c3_Err | |
| 1983 | + | } | |
| 1984 | + | } | |
| 1985 | + | if c.Note != "" { | |
| 1986 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "<p class=\"mute\">") | |
| 1987 | + | if templ_7745c5c3_Err != nil { | |
| 1988 | + | return templ_7745c5c3_Err | |
| 1989 | + | } | |
| 1990 | + | var templ_7745c5c3_Var105 string | |
| 1991 | + | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note) | |
| 1992 | + | if templ_7745c5c3_Err != nil { | |
| 1993 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 275, Col: 27} | |
| 1994 | + | } | |
| 1995 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 1996 | + | if templ_7745c5c3_Err != nil { | |
| 1997 | + | return templ_7745c5c3_Err | |
| 1998 | + | } | |
| 1999 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</p>") | |
| 2000 | + | if templ_7745c5c3_Err != nil { | |
| 2001 | + | return templ_7745c5c3_Err | |
| 2002 | + | } | |
| 2003 | + | } | |
| 2004 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "</section>") | |
| 2005 | + | if templ_7745c5c3_Err != nil { | |
| 2006 | + | return templ_7745c5c3_Err | |
| 2007 | + | } | |
| 2008 | + | return nil | |
| 2009 | + | }) | |
| 2010 | + | } | |
| 2011 | + | ||
| 2012 | + | var _ = templruntime.GeneratedTemplate | |
M internal/adapters/web/commit.go +3 -0
| @@ -44,6 +44,8 @@ type CommitPage struct { | |||
| 44 | 44 | // Invisible is true when some line holds an invisible character, shown as a | |
| 45 | 45 | // marker. | |
| 46 | 46 | Invisible bool | |
| 47 | + | // Checks is the combined state of the CI checks of the commit. | |
| 48 | + | Checks ChecksSummary | |
| 47 | 49 | } | |
| 48 | 50 | ||
| 49 | 51 | // DiffView is how a page lays its diff out: the layouts in the page and the | |
| @@ -138,6 +140,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||
| 138 | 140 | } | |
| 139 | 141 | } | |
| 140 | 142 | p.Note = diffNote(res.Diff, len(p.Files), p.NFiles) | |
| 143 | + | p.Checks = checksSummary(d.repo.Name, cid, s.checksFor(ctx, d.repo.Name, cid)) | |
| 141 | 144 | ||
| 142 | 145 | lines, invisible := diffLines(p.Files) | |
| 143 | 146 | p.Invisible = invisible | |
M internal/adapters/web/commits.go +9 -2
| @@ -73,7 +73,7 @@ func (s *Server) commits(ctx context.Context, r *http.Request) (templ.Component, | |||
| 73 | 73 | if token != "" { | |
| 74 | 74 | p.Newest = base | |
| 75 | 75 | } | |
| 76 | - | s.fillDays(&p, d, cs) | |
| 76 | + | s.fillDays(ctx, &p, d, cs) | |
| 77 | 77 | if sel.path != "" && token == "" { | |
| 78 | 78 | p.Open = s.pathLink(ctx, d, sel) | |
| 79 | 79 | } | |
| @@ -95,14 +95,21 @@ func (s *Server) commits(ctx context.Context, r *http.Request) (templ.Component, | |||
| 95 | 95 | return Commits(l, p), http.StatusOK, nil | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | + | // maxCheckLookups bounds the commits of one page whose checks are read: each | |
| 99 | + | // is one read of the store. | |
| 100 | + | const maxCheckLookups = 100 | |
| 101 | + | ||
| 98 | 102 | // fillDays groups the commits of a page by day and lists the refs among them. | |
| 99 | - | func (s *Server) fillDays(p *CommitsPage, d *repoData, cs []domain.Commit) { | |
| 103 | + | func (s *Server) fillDays(ctx context.Context, p *CommitsPage, d *repoData, cs []domain.Commit) { | |
| 100 | 104 | now := s.cfg.Now() | |
| 101 | 105 | branchesAt, tagsAt := namesAt(d.branches), namesAt(d.tags) | |
| 102 | 106 | for i, c := range cs { | |
| 103 | 107 | row := commitRow(d.name(), c, now) | |
| 104 | 108 | row.ID, row.Cursor = "c-"+row.Short, i == 0 | |
| 105 | 109 | row.Branches, row.Tags = branchesAt[c.ID.String()], tagsAt[c.ID.String()] | |
| 110 | + | if i < maxCheckLookups { | |
| 111 | + | row.Checks = checksSummary(d.repo.Name, c.ID.String(), s.checksFor(ctx, d.repo.Name, c.ID.String())) | |
| 112 | + | } | |
| 106 | 113 | day := "unknown date" | |
| 107 | 114 | if t := c.Committer.Time; !t.IsZero() { | |
| 108 | 115 | day = t.UTC().Format("2006-01-02") | |
M internal/adapters/web/components.templ +1 -0
| @@ -112,6 +112,7 @@ templ CommitRows(rows []CommitRow) { | |||
| 112 | 112 | <span class="pill tag"><span class="sr">tag </span>{ t }</span> | |
| 113 | 113 | } | |
| 114 | 114 | </td> | |
| 115 | + | <td class="c-ck">@CommitChecks(c.Checks)</td> | |
| 115 | 116 | <td class="c-who dim">{ c.Author }</td> | |
| 116 | 117 | <td class="c-when nowrap dim"><time datetime={ c.WhenAt } title={ c.WhenAt }>{ c.When }</time></td> | |
| 117 | 118 | </tr> | |
M internal/adapters/web/components_templ.go +18 -10 generated file: collapsed
M internal/adapters/web/diff.templ +4 -0
| @@ -40,6 +40,10 @@ templ Commit(l Layout, p CommitPage) { | |||
| 40 | 40 | { " · diff against the first parent" } | |
| 41 | 41 | } | |
| 42 | 42 | </dd> | |
| 43 | + | if p.Checks.Total > 0 { | |
| 44 | + | <dt>checks</dt> | |
| 45 | + | <dd>@CommitChecks(p.Checks)</dd> | |
| 46 | + | } | |
| 43 | 47 | if len(p.Refs) > 0 { | |
| 44 | 48 | <dt>refs</dt> | |
| 45 | 49 | <dd> | |
M internal/adapters/web/diff_templ.go +180 -166 generated file: collapsed
M internal/adapters/web/events.go +9 -1
| @@ -31,6 +31,9 @@ import ( | |||
| 31 | 31 | // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha> | |
| 32 | 32 | // GET /-/events?topic=repo:<name>&view=mrs | |
| 33 | 33 | // GET /-/events?topic=repo:<name>&view=mr&n=<number>&ref=<source branch> | |
| 34 | + | // GET /-/events?topic=repo:<name>&view=actions | |
| 35 | + | // GET /-/events?topic=repo:<name>&view=run&n=<run id> | |
| 36 | + | // GET /-/events?topic=repo:<name>&view=job&job=<job id> | |
| 34 | 37 | // | |
| 35 | 38 | // topic is the Watch topic. view says which fragments of a repository the | |
| 36 | 39 | // page shows, so that the server renders only those (summary when absent). | |
| @@ -50,6 +53,11 @@ import ( | |||
| 50 | 53 | // commits-head data: <div id="commits-head">N new commits — reload</div> | |
| 51 | 54 | // mr-notice data: <div id="mr-notice">…</div>, on the merge request pages: | |
| 52 | 55 | // something changed, with the link that reloads the page | |
| 56 | + | // ci-run data: <tr id="run-<id>">…</tr> (the list of runs) or | |
| 57 | + | // <div id="run-body">…</div> (one run) | |
| 58 | + | // ci-job data: <h2 id="job-head">…</h2> and <ol id="job-steps">…</ol>; the log | |
| 59 | + | // itself is never sent in the stream, the page reads the lines after | |
| 60 | + | // the last it holds from /raw?from= | |
| 53 | 61 | // gap data: gap; events were lost, or what the page shows is gone or | |
| 54 | 62 | // changed: the page fetches itself again (ermiteLive in ermite.js) | |
| 55 | 63 | // | |
| @@ -194,7 +202,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) { | |||
| 194 | 202 | return | |
| 195 | 203 | } | |
| 196 | 204 | ||
| 197 | - | st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n")} | |
| 205 | + | st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n"), job: q.Get("job")} | |
| 198 | 206 | handle := func(m recvd) bool { | |
| 199 | 207 | if m.err != nil { | |
| 200 | 208 | if ctx.Err() == nil && !errors.Is(m.err, context.Canceled) { | |
M internal/adapters/web/fragments.go +109 -2
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "context" | |
| 8 | 8 | "errors" | |
| 9 | 9 | "strconv" | |
| 10 | + | "strings" | |
| 10 | 11 | "sync" | |
| 11 | 12 | "time" | |
| 12 | 13 | ||
| @@ -23,7 +24,8 @@ type liveStream struct { | |||
| 23 | 24 | view string // "summary", "refs" or "commits" | |
| 24 | 25 | ref string // commits: the branch listed | |
| 25 | 26 | head string // commits: the commit listed first | |
| 26 | - | n string // mr: the number of the merge request shown | |
| 27 | + | n string // mr: the number of the merge request shown; run: the run id | |
| 28 | + | job string // job: the id of the job shown | |
| 27 | 29 | } | |
| 28 | 30 | ||
| 29 | 31 | // renderFragment renders a component to the data of an event. | |
| @@ -46,7 +48,8 @@ func liveEvent(we ports.WatchEvent) bool { | |||
| 46 | 48 | } | |
| 47 | 49 | switch we.Event.(type) { | |
| 48 | 50 | case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent, | |
| 49 | - | domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.CommentAdded: | |
| 51 | + | domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.CommentAdded, | |
| 52 | + | domain.RunUpdated, domain.JobUpdated: | |
| 50 | 53 | return true | |
| 51 | 54 | } | |
| 52 | 55 | return false | |
| @@ -117,6 +120,10 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error) | |||
| 117 | 120 | // repoPage handles the events of the topic of one repository. | |
| 118 | 121 | func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) { | |
| 119 | 122 | switch ev := we.Event.(type) { | |
| 123 | + | case domain.RunUpdated: | |
| 124 | + | return l.runEvent(ctx, ev) | |
| 125 | + | case domain.JobUpdated: | |
| 126 | + | return l.jobEvent(ctx, ev) | |
| 120 | 127 | case domain.RepoDeleted, domain.RepoCreated: | |
| 121 | 128 | // Gone, or a new repository under the same name: whatever the page | |
| 122 | 129 | // shows is not true any more. | |
| @@ -395,3 +402,103 @@ func (l *liveStream) notice(ctx context.Context, text, href string) ([]fragment, | |||
| 395 | 402 | f, err := renderFragment(ctx, "mr-notice", MRNotice(text, href)) | |
| 396 | 403 | return []fragment{f}, err | |
| 397 | 404 | } | |
| 405 | + | ||
| 406 | + | // runEvent updates the pages of CI that a RunUpdated changes: the row of the | |
| 407 | + | // list of runs, or the body of the run page shown. Only a state is sent: the | |
| 408 | + | // events carry no log and no text of the workflow. | |
| 409 | + | func (l *liveStream) runEvent(ctx context.Context, ev domain.RunUpdated) ([]fragment, error) { | |
| 410 | + | if l.s.cfg.Runs == nil { | |
| 411 | + | return nil, nil | |
| 412 | + | } | |
| 413 | + | switch l.view { | |
| 414 | + | case "actions": | |
| 415 | + | f, err := renderFragment(ctx, "ci-run", RunListRow(l.s.runRow(l.repo, ev.Run, l.s.cfg.Now()))) | |
| 416 | + | if err != nil { | |
| 417 | + | return nil, err | |
| 418 | + | } | |
| 419 | + | if id, ok := rowIsNew(ev.Run); ok { | |
| 420 | + | f.html = strings.Replace(f.html, "<tr id=", "<tr data-new id=", 1) | |
| 421 | + | _ = id | |
| 422 | + | } | |
| 423 | + | return []fragment{f}, nil | |
| 424 | + | case "run": | |
| 425 | + | if strconv.FormatInt(ev.Run.ID, 10) != l.n { | |
| 426 | + | return nil, nil | |
| 427 | + | } | |
| 428 | + | return l.runBody(ctx) | |
| 429 | + | } | |
| 430 | + | return nil, nil | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | // rowIsNew is true for a run created a moment ago: the list adds its row. | |
| 434 | + | func rowIsNew(run domain.CIRun) (int64, bool) { return run.ID, run.Status == domain.CIQueued } | |
| 435 | + | ||
| 436 | + | // jobEvent updates the body of the run page and the steps of the job page. | |
| 437 | + | func (l *liveStream) jobEvent(ctx context.Context, ev domain.JobUpdated) ([]fragment, error) { | |
| 438 | + | if l.s.cfg.Runs == nil { | |
| 439 | + | return nil, nil | |
| 440 | + | } | |
| 441 | + | switch l.view { | |
| 442 | + | case "run": | |
| 443 | + | if strconv.FormatInt(ev.Job.RunID, 10) != l.n { | |
| 444 | + | return nil, nil | |
| 445 | + | } | |
| 446 | + | return l.runBody(ctx) | |
| 447 | + | case "job": | |
| 448 | + | if ev.Job.ID != l.job { | |
| 449 | + | return nil, nil | |
| 450 | + | } | |
| 451 | + | return l.jobState(ctx) | |
| 452 | + | } | |
| 453 | + | return nil, nil | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | // runBody renders again the jobs of the run page. | |
| 457 | + | func (l *liveStream) runBody(ctx context.Context) ([]fragment, error) { | |
| 458 | + | id, err := strconv.ParseInt(l.n, 10, 64) | |
| 459 | + | if err != nil { | |
| 460 | + | return nil, nil | |
| 461 | + | } | |
| 462 | + | d, err := l.s.loadRepo(ctx, l.repo) | |
| 463 | + | if err != nil { | |
| 464 | + | return nil, err | |
| 465 | + | } | |
| 466 | + | p, err := l.s.runPage(ctx, d, id, 0) | |
| 467 | + | if err != nil { | |
| 468 | + | return nil, err | |
| 469 | + | } | |
| 470 | + | body, err := renderFragment(ctx, "ci-run", runBodyFragment(p)) | |
| 471 | + | if err != nil { | |
| 472 | + | return nil, err | |
| 473 | + | } | |
| 474 | + | // The buttons depend on the state: the page fetches itself again when the | |
| 475 | + | // run completed, so that the owner gets the right ones. | |
| 476 | + | if !p.Active { | |
| 477 | + | return []fragment{body, {event: "gap", html: "gap"}}, nil | |
| 478 | + | } | |
| 479 | + | return []fragment{body}, nil | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | // jobState tells the job page that its state or its steps changed: the page | |
| 483 | + | // reloads the part of the log after the last line it holds (ermite.js). | |
| 484 | + | func (l *liveStream) jobState(ctx context.Context) ([]fragment, error) { | |
| 485 | + | d, err := l.s.loadRepo(ctx, l.repo) | |
| 486 | + | if err != nil { | |
| 487 | + | return nil, err | |
| 488 | + | } | |
| 489 | + | view, err := l.s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, l.job, 0) | |
| 490 | + | if err != nil { | |
| 491 | + | return nil, err | |
| 492 | + | } | |
| 493 | + | now := l.s.cfg.Now() | |
| 494 | + | row := l.s.jobRow(d.name(), view, now) | |
| 495 | + | f, err := renderFragment(ctx, "ci-job", JobHead(row, view.Lines)) | |
| 496 | + | if err != nil { | |
| 497 | + | return nil, err | |
| 498 | + | } | |
| 499 | + | steps, err := renderFragment(ctx, "ci-job", JobSteps(stepViews(view, now))) | |
| 500 | + | if err != nil { | |
| 501 | + | return nil, err | |
| 502 | + | } | |
| 503 | + | return []fragment{f, steps}, nil | |
| 504 | + | } | |
M internal/adapters/web/fragments.templ +1 -1
| @@ -101,7 +101,7 @@ templ Live(l Layout) { | |||
| 101 | 101 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 102 | 102 | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 103 | 103 | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 104 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 104 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data); | |
| 105 | 105 | gap -> ermiteLive.reload()" | |
| 106 | 106 | ></span> | |
| 107 | 107 | } | |
M internal/adapters/web/fragments_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.go +8 -0
| @@ -205,6 +205,8 @@ type MRPage struct { | |||
| 205 | 205 | CommitsNote string | |
| 206 | 206 | Modes []string | |
| 207 | 207 | ChecksBlock bool | |
| 208 | + | // Checks are the CI checks of the head of the request. | |
| 209 | + | Checks ChecksSection | |
| 208 | 210 | ||
| 209 | 211 | Box *MergeBox | |
| 210 | 212 | // Live is true when the page announces changes. | |
| @@ -456,6 +458,12 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 456 | 458 | } | |
| 457 | 459 | } | |
| 458 | 460 | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | |
| 461 | + | if open && head != "" { | |
| 462 | + | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now) | |
| 463 | + | } else if head != "" { | |
| 464 | + | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now) | |
| 465 | + | p.Checks.Note = "" | |
| 466 | + | } | |
| 459 | 467 | p.Note = diffNote(res.Diff, len(p.Files), p.NFiles) | |
| 460 | 468 | ||
| 461 | 469 | // The commits of the request, newest first. | |
M internal/adapters/web/mr.templ +1 -5
| @@ -302,11 +302,7 @@ templ mrRail(p MRPage) { | |||
| 302 | 302 | if p.Box != nil { | |
| 303 | 303 | @mergeBox(p) | |
| 304 | 304 | } | |
| 305 | - | <section aria-labelledby="r-checks"> | |
| 306 | - | <h2 id="r-checks">Checks <span class="count">0</span></h2> | |
| 307 | - | <p class="dim">no checks</p> | |
| 308 | - | <p class="mute">there is no CI yet: nothing blocks the merge.</p> | |
| 309 | - | </section> | |
| 305 | + | @MRChecks(p.Checks) | |
| 310 | 306 | <section aria-labelledby="r-settings"> | |
| 311 | 307 | <h2 id="r-settings">Settings</h2> | |
| 312 | 308 | <dl class="kv"> | |
M internal/adapters/web/mr_templ.go +56 -52 generated file: collapsed
M internal/adapters/web/pages.templ +1 -0
| @@ -128,6 +128,7 @@ templ refRow(kind string, r RefRow) { | |||
| 128 | 128 | } else { | |
| 129 | 129 | { r.Short } | |
| 130 | 130 | } | |
| 131 | + | @CommitChecks(r.Checks) | |
| 131 | 132 | </td> | |
| 132 | 133 | <td class="c-sub">{ r.Subject }</td> | |
| 133 | 134 | <td class="c-when dim"> | |
M internal/adapters/web/pages_templ.go +17 -13 generated file: collapsed
M internal/adapters/web/refspage.go +4 -3
| @@ -43,14 +43,14 @@ func (s *Server) refsPage(ctx context.Context, d *repoData) RefsPage { | |||
| 43 | 43 | return a && !b | |
| 44 | 44 | }) | |
| 45 | 45 | for i, b := range branches { | |
| 46 | - | p.Branches = append(p.Branches, s.refRow(d, b, commits, b.ShortName == def, i == 0)) | |
| 46 | + | p.Branches = append(p.Branches, s.refRow(ctx, d, b, commits, b.ShortName == def, i == 0)) | |
| 47 | 47 | } | |
| 48 | 48 | tags := append([]domain.Ref(nil), d.tags...) | |
| 49 | 49 | sort.SliceStable(tags, func(i, j int) bool { | |
| 50 | 50 | return commits[tags[i].CommitID.String()].at > commits[tags[j].CommitID.String()].at | |
| 51 | 51 | }) | |
| 52 | 52 | for i, t := range tags { | |
| 53 | - | p.Tags = append(p.Tags, s.refRow(d, t, commits, false, len(p.Branches) == 0 && i == 0)) | |
| 53 | + | p.Tags = append(p.Tags, s.refRow(ctx, d, t, commits, false, len(p.Branches) == 0 && i == 0)) | |
| 54 | 54 | } | |
| 55 | 55 | if len(tags) > 0 { | |
| 56 | 56 | p.Latest = tags[0].ShortName | |
| @@ -61,7 +61,7 @@ func (s *Server) refsPage(ctx context.Context, d *repoData) RefsPage { | |||
| 61 | 61 | return p | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | - | func (s *Server) refRow(d *repoData, r domain.Ref, commits map[string]refCommit, isDefault, cursor bool) RefRow { | |
| 64 | + | func (s *Server) refRow(ctx context.Context, d *repoData, r domain.Ref, commits map[string]refCommit, isDefault, cursor bool) RefRow { | |
| 65 | 65 | id := r.CommitID.String() | |
| 66 | 66 | c := commits[id] | |
| 67 | 67 | row := RefRow{ | |
| @@ -71,6 +71,7 @@ func (s *Server) refRow(d *repoData, r domain.Ref, commits map[string]refCommit, | |||
| 71 | 71 | } | |
| 72 | 72 | if id != "" { | |
| 73 | 73 | row.CHref = repoHref(d.name(), "commit", id) | |
| 74 | + | row.Checks = checksSummary(d.repo.Name, id, s.checksFor(ctx, d.repo.Name, id)) | |
| 74 | 75 | } | |
| 75 | 76 | return row | |
| 76 | 77 | } | |
M internal/adapters/web/repo.go +9 -0
| @@ -135,6 +135,7 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||
| 135 | 135 | if s.cfg.MergeRequests != nil { | |
| 136 | 136 | l.Sections = append(l.Sections, mk("m", "Merge requests", mrBase(d.name()), "merge-requests")) | |
| 137 | 137 | } | |
| 138 | + | l.Sections = s.withActions(l.Sections, d.name(), section) | |
| 138 | 139 | l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}} | |
| 139 | 140 | ||
| 140 | 141 | l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"}) | |
| @@ -161,6 +162,14 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||
| 161 | 162 | return l | |
| 162 | 163 | } | |
| 163 | 164 | ||
| 165 | + | // withActions adds the Actions tab when the server runs CI. | |
| 166 | + | func (s *Server) withActions(sections []Section, repo, current string) []Section { | |
| 167 | + | if s.cfg.Runs == nil { | |
| 168 | + | return sections | |
| 169 | + | } | |
| 170 | + | return append(sections, Section{Key: "a", Label: "Actions", Href: actionsBase(repo), Current: current == "actions"}) | |
| 171 | + | } | |
| 172 | + | ||
| 164 | 173 | // cloneURL is the HTTPS clone URL of a repository. | |
| 165 | 174 | func (s *Server) cloneURL(name string) string { | |
| 166 | 175 | return s.cfg.PublicURL + "/" + name + ".git" | |
A internal/adapters/web/static/css/65-ci.css +53 -0
| @@ -0,0 +1,53 @@ | |||
| 1 | + | @layer components { | |
| 2 | + | /* CI: runs, jobs, steps, checks and the log. Hue only where it carries a | |
| 3 | + | verdict, and every mark has its word beside it or in its label. */ | |
| 4 | + | .ck { | |
| 5 | + | display: inline-block; width: 14px; height: 14px; margin-right: var(--s1); vertical-align: -2px; | |
| 6 | + | border: 2px solid var(--ink-dim); border-radius: 50%; box-sizing: border-box; flex: none; | |
| 7 | + | } | |
| 8 | + | .ck-ok { background: var(--teal); border-color: var(--teal); } | |
| 9 | + | .ck-bad { background: var(--red); border-color: var(--red); border-radius: 3px; transform: rotate(45deg) scale(.85); } | |
| 10 | + | .ck-cancelled { border-style: dashed; } | |
| 11 | + | .ck-skipped { border-color: var(--border-strong); background: var(--bg-raised); } | |
| 12 | + | .ck-queued { border-style: dotted; } | |
| 13 | + | .ck-running { border-color: var(--teal); border-top-color: transparent; animation: ck-spin 1s linear infinite; } | |
| 14 | + | @keyframes ck-spin { to { transform: rotate(360deg); } } | |
| 15 | + | @media (prefers-reduced-motion: reduce) { .ck-running { animation: none; border-top-color: var(--teal); } } | |
| 16 | + | ||
| 17 | + | .ck-sum { display: inline-flex; align-items: center; gap: var(--s1); margin-left: var(--s2); font-size: var(--fs-xs); white-space: nowrap; } | |
| 18 | + | .c-ck { width: 1%; white-space: nowrap; } | |
| 19 | + | ||
| 20 | + | .runs .c-st { width: 1%; } | |
| 21 | + | .runs .c-dur, .runs .c-who { white-space: nowrap; } | |
| 22 | + | .runs tr[data-new] { background: var(--bg-raised); } | |
| 23 | + | ||
| 24 | + | .run-h { display: flex; align-items: center; gap: var(--s2); margin: 0 0 var(--s4); } | |
| 25 | + | .run-err, .job-err { | |
| 26 | + | max-width: var(--measure); padding: var(--s3) var(--s4); margin: 0 0 var(--s4); white-space: pre-wrap; overflow-wrap: anywhere; | |
| 27 | + | background: var(--bg-surface); border: 1px solid var(--border); border-left: 3px solid var(--red); border-radius: var(--r-s); | |
| 28 | + | font: inherit; font-size: var(--fs-s); | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | .jobs { display: grid; gap: var(--s4); padding: 0; list-style: none; } | |
| 32 | + | .jobs > li { padding: var(--s3) var(--s4); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } | |
| 33 | + | .job-h { display: flex; align-items: center; gap: var(--s2); font-weight: 700; } | |
| 34 | + | .steps { display: grid; gap: 2px; padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | |
| 35 | + | .steps li { display: flex; align-items: center; gap: var(--s1); } | |
| 36 | + | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | |
| 37 | + | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | |
| 38 | + | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | |
| 39 | + | ||
| 40 | + | .joblog { margin-top: var(--s4); } | |
| 41 | + | .log { | |
| 42 | + | margin: 0; padding: var(--s2) 0; list-style: none; overflow-x: auto; | |
| 43 | + | font: var(--fs-s) / 1.5 var(--mono); background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--r-s); | |
| 44 | + | } | |
| 45 | + | .log .lg { display: flex; gap: var(--s3); padding: 0 var(--s3); white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 46 | + | .log .lg:target { background: var(--bg-raised); } | |
| 47 | + | .log .lg time { flex: none; color: var(--ink-dim); user-select: none; } | |
| 48 | + | .log .lg .t { min-width: 0; } | |
| 49 | + | .log .group .t { font-weight: 700; } | |
| 50 | + | .log .endgroup { display: none; } | |
| 51 | + | .log .error .t { color: var(--red); } | |
| 52 | + | .joblog .more { margin: var(--s2) 0; font-size: var(--fs-s); } | |
| 53 | + | } | |
M internal/adapters/web/static/js/ermite.js +23 -0
| @@ -363,3 +363,26 @@ window.ermiteLive = (() => { | |||
| 363 | 363 | }, | |
| 364 | 364 | }; | |
| 365 | 365 | })(); | |
| 366 | + | ||
| 367 | + | // ermiteLog follows the log of a running job on its page: the lines arrive as | |
| 368 | + | // "ci-log" events (internal/adapters/web/actions.go, logStream) and are | |
| 369 | + | // appended to the list. The browser reconnects by itself with Last-Event-ID, | |
| 370 | + | // the number of the next line; "ci-end" closes the feed when the job is over. | |
| 371 | + | // The page scrolls with the log only while the reader is at its end. | |
| 372 | + | (() => { | |
| 373 | + | const box = document.getElementById('job-log'); | |
| 374 | + | if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return; | |
| 375 | + | const list = box.querySelector('ol.log'); | |
| 376 | + | if (!list) return; | |
| 377 | + | const url = box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0'); | |
| 378 | + | const es = new EventSource(url); | |
| 379 | + | es.addEventListener('ci-log', (e) => { | |
| 380 | + | const t = document.createElement('template'); | |
| 381 | + | t.innerHTML = e.data; | |
| 382 | + | const atEnd = window.innerHeight + window.scrollY >= document.body.scrollHeight - 80; | |
| 383 | + | list.append(...t.content.children); | |
| 384 | + | if (atEnd) window.scrollTo(0, document.body.scrollHeight); | |
| 385 | + | }); | |
| 386 | + | es.addEventListener('ci-end', () => es.close()); | |
| 387 | + | es.onerror = () => { if (es.readyState === EventSource.CLOSED) es.close(); }; | |
| 388 | + | })(); | |
M internal/adapters/web/view.go +5 -0
| @@ -135,6 +135,9 @@ type CommitRow struct { | |||
| 135 | 135 | Branches []string | |
| 136 | 136 | Tags []string | |
| 137 | 137 | Cursor bool | |
| 138 | + | // Checks is the combined state of the CI checks of the commit; empty when | |
| 139 | + | // it has none. | |
| 140 | + | Checks ChecksSummary | |
| 138 | 141 | } | |
| 139 | 142 | ||
| 140 | 143 | // SummaryPage is the repository summary. | |
| @@ -162,6 +165,8 @@ type LatestTag struct { | |||
| 162 | 165 | type RefRow struct { | |
| 163 | 166 | Name, Href, Short, CHref, Subject, When, WhenAt string | |
| 164 | 167 | Default, Cursor bool | |
| 168 | + | // Checks is the combined state of the CI checks of the commit. | |
| 169 | + | Checks ChecksSummary | |
| 165 | 170 | } | |
| 166 | 171 | ||
| 167 | 172 | // RefsPage lists the branches and tags of a repository. | |
M internal/adapters/web/web.go +15 -0
| @@ -54,6 +54,10 @@ type Config struct { | |||
| 54 | 54 | // there are no such pages, nor section for them. Leave it unset rather | |
| 55 | 55 | // than assigning a typed nil pointer. | |
| 56 | 56 | MergeRequests ports.MergeRequestService | |
| 57 | + | // Runs serves the CI pages (/<repo>/actions) and the checks shown next to | |
| 58 | + | // commits and merge requests. Without it there are no such pages. Leave it | |
| 59 | + | // unset rather than assigning a typed nil pointer. | |
| 60 | + | Runs ports.RunService | |
| 57 | 61 | // Events is the source of the event stream behind /-/events. Without | |
| 58 | 62 | // it the pages are not live and the route answers 404. Leave it unset | |
| 59 | 63 | // rather than assigning a typed nil pointer. | |
| @@ -161,6 +165,17 @@ func (s *Server) routes() *http.ServeMux { | |||
| 161 | 165 | mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose)) | |
| 162 | 166 | mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead)) | |
| 163 | 167 | } | |
| 168 | + | if s.cfg.Runs != nil { | |
| 169 | + | mux.HandleFunc("GET /{repo}/actions", s.page(s.actions)) | |
| 170 | + | mux.HandleFunc("GET /{repo}/actions/{$}", s.page(s.actions)) | |
| 171 | + | mux.HandleFunc("GET /{repo}/actions/runs/{id}", s.page(s.run)) | |
| 172 | + | mux.HandleFunc("GET /{repo}/actions/runs/{id}/job/{job}", s.page(s.job)) | |
| 173 | + | mux.HandleFunc("GET /{repo}/actions/runs/{id}/job/{job}/raw", s.rawLog) | |
| 174 | + | mux.HandleFunc("GET /{repo}/actions/runs/{id}/job/{job}/stream", s.logStream) | |
| 175 | + | mux.HandleFunc("POST /{repo}/actions/runs/{id}/cancel", s.ciAction(s.runCancel)) | |
| 176 | + | mux.HandleFunc("POST /{repo}/actions/runs/{id}/rerun", s.ciAction(s.runRerun(false))) | |
| 177 | + | mux.HandleFunc("POST /{repo}/actions/runs/{id}/rerun-failed", s.ciAction(s.runRerun(true))) | |
| 178 | + | } | |
| 164 | 179 | mux.HandleFunc("GET /{repo}/{rest...}", s.page(s.notFound)) | |
| 165 | 180 | return mux | |
| 166 | 181 | } | |
A internal/app/ci_web_test.go +174 -0
| @@ -0,0 +1,174 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package app | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "net/url" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "connectrpc.com/connect" | |
| 12 | + | "github.com/stretchr/testify/assert" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | "google.golang.org/protobuf/types/known/fieldmaskpb" | |
| 15 | + | ||
| 16 | + | pb "bnema.dev/ermite-api/gen/ermite" | |
| 17 | + | "bnema.dev/ermite/internal/domain" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // AC5, AC6: the pages of CI: the list of runs, a run, a job with its log (page, | |
| 21 | + | // raw and stream), the owner's buttons, and the checks on the commit, the | |
| 22 | + | // branch list, the commit list and the merge request. | |
| 23 | + | func TestWebActionsPages(t *testing.T) { | |
| 24 | + | ci := newCIHarness(t, nil) | |
| 25 | + | c := &cliHarness{harness: ci.harness, url: ci.srv.URL} | |
| 26 | + | b := browser{ci.harness} | |
| 27 | + | runner := ci.registerRunner("alpha", "ubuntu-latest") | |
| 28 | + | _, err := ci.root.repo.UpdateRepo(ci.ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | |
| 29 | + | Repo: "proj", Visibility: pb.Visibility_VISIBILITY_PUBLIC, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"visibility"}}, | |
| 30 | + | })) | |
| 31 | + | require.NoError(t, err) | |
| 32 | + | ci.push("main", map[string]string{"README": "x\n"}, "") | |
| 33 | + | sha := ci.push("feat", map[string]string{".github/workflows/ci.yml": ciWorkflow}, "add ci") | |
| 34 | + | ci.waitFor("the run", func() bool { return len(ci.runs()) == 1 }) | |
| 35 | + | ||
| 36 | + | // Queued: the list shows it, with a word for its state. | |
| 37 | + | list := b.get("/proj/actions", nil) | |
| 38 | + | require.Equal(t, 200, list.status, list.body) | |
| 39 | + | assert.Contains(t, list.body, "queued") | |
| 40 | + | assert.Contains(t, list.body, `href="/proj/actions/runs/1"`) | |
| 41 | + | assert.Contains(t, list.body, `aria-label="queued"`) | |
| 42 | + | assert.Contains(t, list.body, "/-/events?topic=repo%3Aproj&view=actions", "the page follows the repository stream") | |
| 43 | + | assert.Contains(t, list.body, ">Actions<", "the tab") | |
| 44 | + | ||
| 45 | + | // The MR page says "waiting for a runner" only when no runner matches. | |
| 46 | + | mr, err := ci.root.mr.CreateMergeRequest(ci.ctx, connect.NewRequest(&pb.CreateMergeRequestRequest{Repo: "proj", Title: "Add CI", SourceBranch: "feat"})) | |
| 47 | + | require.NoError(t, err) | |
| 48 | + | mrPage := b.get("/proj/merge-requests/1", nil) | |
| 49 | + | require.Equal(t, 200, mrPage.status) | |
| 50 | + | assert.Contains(t, mrPage.body, `id="mr-checks"`) | |
| 51 | + | assert.Contains(t, mrPage.body, "build") | |
| 52 | + | assert.Contains(t, mrPage.body, "a failing or unfinished check blocks the merge", "the rule shown is the one that applies") | |
| 53 | + | assert.NotContains(t, mrPage.body, "waiting for a runner", "a runner matches") | |
| 54 | + | _ = mr | |
| 55 | + | ||
| 56 | + | job := runner.await() | |
| 57 | + | runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "##[group]Run echo hello", "hello <b>bold</b>", "##[endgroup]") | |
| 58 | + | next := runner.await() | |
| 59 | + | runner.runJob(next, pb.CiConclusion_CI_CONCLUSION_FAILURE, nil, "##[error]Process completed with exit code 1.") | |
| 60 | + | ci.waitFor("the run completes", func() bool { return ci.runs()[0].GetStatus() == pb.CiStatus_CI_STATUS_COMPLETED }) | |
| 61 | + | ||
| 62 | + | run := b.get("/proj/actions/runs/1", nil) | |
| 63 | + | require.Equal(t, 200, run.status, run.body) | |
| 64 | + | assert.Contains(t, run.body, "build") | |
| 65 | + | assert.Contains(t, run.body, "test") | |
| 66 | + | assert.Contains(t, run.body, "failure") | |
| 67 | + | assert.Contains(t, run.body, "on alpha (vm)", "the runner and its isolation are named") | |
| 68 | + | assert.NotContains(t, run.body, `action="/proj/actions/runs/1/rerun"`, "no button for a visitor") | |
| 69 | + | ||
| 70 | + | jobPage := b.get("/proj/actions/runs/1/job/"+job.GetJobId(), nil) | |
| 71 | + | require.Equal(t, 200, jobPage.status, jobPage.body) | |
| 72 | + | assert.Contains(t, jobPage.body, "Run echo hello") | |
| 73 | + | assert.Contains(t, jobPage.body, "hello <b>bold</b>", "log text is escaped, never markup") | |
| 74 | + | assert.NotContains(t, jobPage.body, "<b>bold</b>") | |
| 75 | + | assert.NotContains(t, jobPage.body, "##[group]", "markers are shown as structure") | |
| 76 | + | assert.Contains(t, jobPage.body, `class="lg group"`) | |
| 77 | + | failed := b.get("/proj/actions/runs/1/job/"+next.GetJobId(), nil) | |
| 78 | + | assert.Contains(t, failed.body, `class="lg error"`) | |
| 79 | + | ||
| 80 | + | raw := b.get("/proj/actions/runs/1/job/"+job.GetJobId()+"/raw", nil) | |
| 81 | + | require.Equal(t, 200, raw.status) | |
| 82 | + | assert.Equal(t, "text/plain; charset=utf-8", raw.hdr.Get("Content-Type")) | |
| 83 | + | assert.Equal(t, "nosniff", raw.hdr.Get("X-Content-Type-Options")) | |
| 84 | + | assert.Contains(t, raw.hdr.Get("Content-Security-Policy"), "sandbox") | |
| 85 | + | assert.Contains(t, raw.body, "hello <b>bold</b>", "raw is raw text") | |
| 86 | + | assert.Regexp(t, `(?m)^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z ##\[group\]Run echo hello$`, raw.body) | |
| 87 | + | ||
| 88 | + | // The stream of a finished job sends its lines then ends. | |
| 89 | + | stream := b.get("/proj/actions/runs/1/job/"+job.GetJobId()+"/stream?from=1", nil) | |
| 90 | + | require.Equal(t, 200, stream.status) | |
| 91 | + | assert.Equal(t, "text/event-stream", stream.hdr.Get("Content-Type")) | |
| 92 | + | assert.Contains(t, stream.body, "event: ci-log") | |
| 93 | + | assert.Contains(t, stream.body, "event: ci-end") | |
| 94 | + | assert.NotContains(t, stream.body, "Run echo hello", "from=1 skips the first line") | |
| 95 | + | ||
| 96 | + | // Checks next to the commit, the commits list and the branch list. | |
| 97 | + | commit := b.get("/proj/commit/"+sha, nil) | |
| 98 | + | require.Equal(t, 200, commit.status) | |
| 99 | + | assert.Contains(t, commit.body, "checks failed") | |
| 100 | + | assert.Contains(t, b.get("/proj/commits/feat", nil).body, "checks failed") | |
| 101 | + | assert.Contains(t, b.get("/proj/refs", nil).body, "checks failed") | |
| 102 | + | ||
| 103 | + | // Private repository: nothing to anonymous. | |
| 104 | + | _, err = ci.root.repo.UpdateRepo(ci.ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | |
| 105 | + | Repo: "proj", Visibility: pb.Visibility_VISIBILITY_PRIVATE, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"visibility"}}, | |
| 106 | + | })) | |
| 107 | + | require.NoError(t, err) | |
| 108 | + | for _, p := range []string{"/proj/actions", "/proj/actions/runs/1", "/proj/actions/runs/1/job/" + job.GetJobId(), "/proj/actions/runs/1/job/" + job.GetJobId() + "/raw", "/proj/actions/runs/1/job/" + job.GetJobId() + "/stream"} { | |
| 109 | + | assert.Equal(t, 404, b.get(p, nil).status, p) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // The owner sees the buttons and re-runs the failed job. | |
| 113 | + | token := c.signIn(t) | |
| 114 | + | owner := b.get("/proj/actions/runs/1", as(token)) | |
| 115 | + | require.Equal(t, 200, owner.status) | |
| 116 | + | assert.Contains(t, owner.body, `action="/proj/actions/runs/1/rerun"`) | |
| 117 | + | assert.Contains(t, owner.body, `action="/proj/actions/runs/1/rerun-failed"`) | |
| 118 | + | assert.NotContains(t, owner.body, `action="/proj/actions/runs/1/cancel"`) | |
| 119 | + | // A POST needs the owner's session and the origin. | |
| 120 | + | assert.Equal(t, 403, b.do("POST", "/proj/actions/runs/1/rerun-failed", map[string]string{"Origin": publicOrigin}, "").status, "anonymous") | |
| 121 | + | res := b.form("/proj/actions/runs/1/rerun-failed", token, url.Values{}) | |
| 122 | + | require.Equal(t, 303, res.status, res.body) | |
| 123 | + | ci.waitFor("attempt 2 is queued", func() bool { return ci.runs()[0].GetAttempt() == 2 }) | |
| 124 | + | again := b.get("/proj/actions/runs/1?attempt=1", as(token)) | |
| 125 | + | assert.Contains(t, again.body, "attempt 1") | |
| 126 | + | assert.NotContains(t, again.body, `action="/proj/actions/runs/1/rerun"`, "an earlier attempt has no button") | |
| 127 | + | ||
| 128 | + | cancel := b.form("/proj/actions/runs/1/cancel", token, url.Values{}) | |
| 129 | + | require.Equal(t, 303, cancel.status, cancel.body) | |
| 130 | + | ci.waitFor("cancelled", func() bool { | |
| 131 | + | return ci.runs()[0].GetStatus() == pb.CiStatus_CI_STATUS_COMPLETED && ci.runs()[0].GetConclusion() == pb.CiConclusion_CI_CONCLUSION_CANCELLED | |
| 132 | + | }) | |
| 133 | + | assert.Equal(t, 409, b.form("/proj/actions/runs/1/cancel", token, url.Values{}).status, "a completed run cannot be cancelled") | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // The live updates of the pages: the run page gets the body of the run when a | |
| 137 | + | // job changes, and the job page its state, never a line of the log. | |
| 138 | + | func TestWebActionsLiveFragments(t *testing.T) { | |
| 139 | + | ci := newCIHarness(t, nil) | |
| 140 | + | c := &cliHarness{harness: ci.harness, url: ci.srv.URL} | |
| 141 | + | runner := ci.registerRunner("alpha", "ubuntu-latest") | |
| 142 | + | _, err := ci.root.repo.UpdateRepo(ci.ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | |
| 143 | + | Repo: "proj", Visibility: pb.Visibility_VISIBILITY_PUBLIC, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"visibility"}}, | |
| 144 | + | })) | |
| 145 | + | require.NoError(t, err) | |
| 146 | + | ci.push("main", map[string]string{".github/workflows/ci.yml": strings.Split(ciWorkflow, " test:")[0]}, "") | |
| 147 | + | ci.waitFor("the run", func() bool { return len(ci.runs()) == 1 }) | |
| 148 | + | token := c.signIn(t) | |
| 149 | + | ||
| 150 | + | run := ci.sse("topic=repo:proj&view=run&n=1", "") | |
| 151 | + | list := ci.sse("topic=repo:proj&view=actions", "") | |
| 152 | + | job := runner.await() | |
| 153 | + | jobStream := ci.sse("topic=repo:proj&view=job&job="+job.GetJobId(), "") | |
| 154 | + | _ = token | |
| 155 | + | ||
| 156 | + | runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "a secret value in the log") | |
| 157 | + | deadline := time.After(10 * time.Second) | |
| 158 | + | var sawRun, sawList, sawJob bool | |
| 159 | + | for !sawRun || !sawList || !sawJob { | |
| 160 | + | select { | |
| 161 | + | case ev := <-run.events: | |
| 162 | + | sawRun = sawRun || (ev.name == "ci-run" && strings.Contains(ev.data, `id="run-body"`)) | |
| 163 | + | assert.NotContains(t, ev.data, "a secret value in the log") | |
| 164 | + | case ev := <-list.events: | |
| 165 | + | sawList = sawList || (ev.name == "ci-run" && strings.Contains(ev.data, `id="run-1"`)) | |
| 166 | + | case ev := <-jobStream.events: | |
| 167 | + | sawJob = sawJob || (ev.name == "ci-job" && strings.Contains(ev.data, `id="job-head"`)) | |
| 168 | + | assert.NotContains(t, ev.data, "a secret value in the log", "the stream of states never carries the log") | |
| 169 | + | case <-deadline: | |
| 170 | + | t.Fatalf("fragments: run %v, list %v, job %v", sawRun, sawList, sawJob) | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | _ = domain.CISuccess | |
| 174 | + | } | |
M internal/app/testdata/web/blob-binary.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -308,6 +312,12 @@ | |||
| 308 | 312 | <span class="k">section</span> | |
| 309 | 313 | </a> | |
| 310 | 314 | </li> | |
| 315 | + | <li data-k="section actions"> | |
| 316 | + | <a href="/files/actions" tabindex="-1"> | |
| 317 | + | <span class="lbl">actions</span> | |
| 318 | + | <span class="k">section</span> | |
| 319 | + | </a> | |
| 320 | + | </li> | |
| 311 | 321 | <li data-k="branch feature/x"> | |
| 312 | 322 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 313 | 323 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-code.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -378,6 +382,12 @@ | |||
| 378 | 382 | <span class="k">section</span> | |
| 379 | 383 | </a> | |
| 380 | 384 | </li> | |
| 385 | + | <li data-k="section actions"> | |
| 386 | + | <a href="/files/actions" tabindex="-1"> | |
| 387 | + | <span class="lbl">actions</span> | |
| 388 | + | <span class="k">section</span> | |
| 389 | + | </a> | |
| 390 | + | </li> | |
| 381 | 391 | <li data-k="branch feature/x"> | |
| 382 | 392 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 383 | 393 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-empty.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -308,6 +312,12 @@ | |||
| 308 | 312 | <span class="k">section</span> | |
| 309 | 313 | </a> | |
| 310 | 314 | </li> | |
| 315 | + | <li data-k="section actions"> | |
| 316 | + | <a href="/files/actions" tabindex="-1"> | |
| 317 | + | <span class="lbl">actions</span> | |
| 318 | + | <span class="k">section</span> | |
| 319 | + | </a> | |
| 320 | + | </li> | |
| 311 | 321 | <li data-k="branch feature/x"> | |
| 312 | 322 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 313 | 323 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-image.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -310,6 +314,12 @@ | |||
| 310 | 314 | <span class="k">section</span> | |
| 311 | 315 | </a> | |
| 312 | 316 | </li> | |
| 317 | + | <li data-k="section actions"> | |
| 318 | + | <a href="/files/actions" tabindex="-1"> | |
| 319 | + | <span class="lbl">actions</span> | |
| 320 | + | <span class="k">section</span> | |
| 321 | + | </a> | |
| 322 | + | </li> | |
| 313 | 323 | <li data-k="branch feature/x"> | |
| 314 | 324 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 315 | 325 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-markdown.html +10 -0
| @@ -86,6 +86,10 @@ | |||
| 86 | 86 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 87 | 87 | </a> | |
| 88 | 88 | </li> | |
| 89 | + | <li> | |
| 90 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 91 | + | </a> | |
| 92 | + | </li> | |
| 89 | 93 | </ul> | |
| 90 | 94 | </nav> | |
| 91 | 95 | </header> | |
| @@ -375,6 +379,12 @@ | |||
| 375 | 379 | <span class="k">section</span> | |
| 376 | 380 | </a> | |
| 377 | 381 | </li> | |
| 382 | + | <li data-k="section actions"> | |
| 383 | + | <a href="/files/actions" tabindex="-1"> | |
| 384 | + | <span class="lbl">actions</span> | |
| 385 | + | <span class="k">section</span> | |
| 386 | + | </a> | |
| 387 | + | </li> | |
| 378 | 388 | <li data-k="branch feature/x"> | |
| 379 | 389 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 380 | 390 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-svg.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -333,6 +337,12 @@ | |||
| 333 | 337 | <span class="k">section</span> | |
| 334 | 338 | </a> | |
| 335 | 339 | </li> | |
| 340 | + | <li data-k="section actions"> | |
| 341 | + | <a href="/files/actions" tabindex="-1"> | |
| 342 | + | <span class="lbl">actions</span> | |
| 343 | + | <span class="k">section</span> | |
| 344 | + | </a> | |
| 345 | + | </li> | |
| 336 | 346 | <li data-k="branch feature/x"> | |
| 337 | 347 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 338 | 348 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-text.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -328,6 +332,12 @@ | |||
| 328 | 332 | <span class="k">section</span> | |
| 329 | 333 | </a> | |
| 330 | 334 | </li> | |
| 335 | + | <li data-k="section actions"> | |
| 336 | + | <a href="/files/actions" tabindex="-1"> | |
| 337 | + | <span class="lbl">actions</span> | |
| 338 | + | <span class="k">section</span> | |
| 339 | + | </a> | |
| 340 | + | </li> | |
| 331 | 341 | <li data-k="branch feature/x"> | |
| 332 | 342 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 333 | 343 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-too-large.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -308,6 +312,12 @@ | |||
| 308 | 312 | <span class="k">section</span> | |
| 309 | 313 | </a> | |
| 310 | 314 | </li> | |
| 315 | + | <li data-k="section actions"> | |
| 316 | + | <a href="/files/actions" tabindex="-1"> | |
| 317 | + | <span class="lbl">actions</span> | |
| 318 | + | <span class="k">section</span> | |
| 319 | + | </a> | |
| 320 | + | </li> | |
| 311 | 321 | <li data-k="branch feature/x"> | |
| 312 | 322 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 313 | 323 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/blob-trojan.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -375,6 +379,12 @@ | |||
| 375 | 379 | <span class="k">section</span> | |
| 376 | 380 | </a> | |
| 377 | 381 | </li> | |
| 382 | + | <li data-k="section actions"> | |
| 383 | + | <a href="/files/actions" tabindex="-1"> | |
| 384 | + | <span class="lbl">actions</span> | |
| 385 | + | <span class="k">section</span> | |
| 386 | + | </a> | |
| 387 | + | </li> | |
| 378 | 388 | <li data-k="branch feature/x"> | |
| 379 | 389 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 380 | 390 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/commit-big.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -345,6 +349,12 @@ | |||
| 345 | 349 | <span class="k">section</span> | |
| 346 | 350 | </a> | |
| 347 | 351 | </li> | |
| 352 | + | <li data-k="section actions"> | |
| 353 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 354 | + | <span class="lbl">actions</span> | |
| 355 | + | <span class="k">section</span> | |
| 356 | + | </a> | |
| 357 | + | </li> | |
| 348 | 358 | <li data-k="branch main"> | |
| 349 | 359 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 350 | 360 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-binary.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -340,6 +344,12 @@ | |||
| 340 | 344 | <span class="k">section</span> | |
| 341 | 345 | </a> | |
| 342 | 346 | </li> | |
| 347 | + | <li data-k="section actions"> | |
| 348 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 349 | + | <span class="lbl">actions</span> | |
| 350 | + | <span class="k">section</span> | |
| 351 | + | </a> | |
| 352 | + | </li> | |
| 343 | 353 | <li data-k="branch main"> | |
| 344 | 354 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 345 | 355 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-merge.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -379,6 +383,12 @@ | |||
| 379 | 383 | <span class="k">section</span> | |
| 380 | 384 | </a> | |
| 381 | 385 | </li> | |
| 386 | + | <li data-k="section actions"> | |
| 387 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 388 | + | <span class="lbl">actions</span> | |
| 389 | + | <span class="k">section</span> | |
| 390 | + | </a> | |
| 391 | + | </li> | |
| 382 | 392 | <li data-k="branch main"> | |
| 383 | 393 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 384 | 394 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-modify-split.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -500,6 +504,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 500 | 504 | <span class="k">section</span> | |
| 501 | 505 | </a> | |
| 502 | 506 | </li> | |
| 507 | + | <li data-k="section actions"> | |
| 508 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 509 | + | <span class="lbl">actions</span> | |
| 510 | + | <span class="k">section</span> | |
| 511 | + | </a> | |
| 512 | + | </li> | |
| 503 | 513 | <li data-k="branch main"> | |
| 504 | 514 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 505 | 515 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-modify.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -500,6 +504,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 500 | 504 | <span class="k">section</span> | |
| 501 | 505 | </a> | |
| 502 | 506 | </li> | |
| 507 | + | <li data-k="section actions"> | |
| 508 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 509 | + | <span class="lbl">actions</span> | |
| 510 | + | <span class="k">section</span> | |
| 511 | + | </a> | |
| 512 | + | </li> | |
| 503 | 513 | <li data-k="branch main"> | |
| 504 | 514 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 505 | 515 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-rename.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -405,6 +409,12 @@ | |||
| 405 | 409 | <span class="k">section</span> | |
| 406 | 410 | </a> | |
| 407 | 411 | </li> | |
| 412 | + | <li data-k="section actions"> | |
| 413 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 414 | + | <span class="lbl">actions</span> | |
| 415 | + | <span class="k">section</span> | |
| 416 | + | </a> | |
| 417 | + | </li> | |
| 408 | 418 | <li data-k="branch main"> | |
| 409 | 419 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 410 | 420 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-root.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -721,6 +725,12 @@ | |||
| 721 | 725 | <span class="k">section</span> | |
| 722 | 726 | </a> | |
| 723 | 727 | </li> | |
| 728 | + | <li data-k="section actions"> | |
| 729 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 730 | + | <span class="lbl">actions</span> | |
| 731 | + | <span class="k">section</span> | |
| 732 | + | </a> | |
| 733 | + | </li> | |
| 724 | 734 | <li data-k="branch main"> | |
| 725 | 735 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 726 | 736 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commit-trojan.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -514,6 +518,12 @@ | |||
| 514 | 518 | <span class="k">section</span> | |
| 515 | 519 | </a> | |
| 516 | 520 | </li> | |
| 521 | + | <li data-k="section actions"> | |
| 522 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 523 | + | <span class="lbl">actions</span> | |
| 524 | + | <span class="k">section</span> | |
| 525 | + | </a> | |
| 526 | + | </li> | |
| 517 | 527 | <li data-k="branch main"> | |
| 518 | 528 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 519 | 529 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commits-page2.html +14 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -103,6 +107,8 @@ | |||
| 103 | 107 | <td class="c-subject"> | |
| 104 | 108 | <a href="/long/commit/SHA40">commit number 1</a> | |
| 105 | 109 | </td> | |
| 110 | + | <td class="c-ck"> | |
| 111 | + | </td> | |
| 106 | 112 | <td class="c-who dim">T</td> | |
| 107 | 113 | <td class="c-when nowrap dim"> | |
| 108 | 114 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -115,6 +121,8 @@ | |||
| 115 | 121 | <td class="c-subject"> | |
| 116 | 122 | <a href="/long/commit/SHA40">commit number 0</a> | |
| 117 | 123 | </td> | |
| 124 | + | <td class="c-ck"> | |
| 125 | + | </td> | |
| 118 | 126 | <td class="c-who dim">T</td> | |
| 119 | 127 | <td class="c-when nowrap dim"> | |
| 120 | 128 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -327,6 +335,12 @@ | |||
| 327 | 335 | <span class="k">section</span> | |
| 328 | 336 | </a> | |
| 329 | 337 | </li> | |
| 338 | + | <li data-k="section actions"> | |
| 339 | + | <a href="/long/actions" tabindex="-1"> | |
| 340 | + | <span class="lbl">actions</span> | |
| 341 | + | <span class="k">section</span> | |
| 342 | + | </a> | |
| 343 | + | </li> | |
| 330 | 344 | <li data-k="branch main"> | |
| 331 | 345 | <a href="/long/tree/main" tabindex="-1"> | |
| 332 | 346 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commits-path.html +14 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -107,6 +111,8 @@ | |||
| 107 | 111 | <td class="c-subject"> | |
| 108 | 112 | <a href="/long/commit/SHA40">commit number 30</a> | |
| 109 | 113 | </td> | |
| 114 | + | <td class="c-ck"> | |
| 115 | + | </td> | |
| 110 | 116 | <td class="c-who dim">T</td> | |
| 111 | 117 | <td class="c-when nowrap dim"> | |
| 112 | 118 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -128,6 +134,8 @@ | |||
| 128 | 134 | <td class="c-subject"> | |
| 129 | 135 | <a href="/long/commit/SHA40">commit number 3</a> | |
| 130 | 136 | </td> | |
| 137 | + | <td class="c-ck"> | |
| 138 | + | </td> | |
| 131 | 139 | <td class="c-who dim">T</td> | |
| 132 | 140 | <td class="c-when nowrap dim"> | |
| 133 | 141 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -342,6 +350,12 @@ | |||
| 342 | 350 | <span class="k">section</span> | |
| 343 | 351 | </a> | |
| 344 | 352 | </li> | |
| 353 | + | <li data-k="section actions"> | |
| 354 | + | <a href="/long/actions" tabindex="-1"> | |
| 355 | + | <span class="lbl">actions</span> | |
| 356 | + | <span class="k">section</span> | |
| 357 | + | </a> | |
| 358 | + | </li> | |
| 345 | 359 | <li data-k="branch main"> | |
| 346 | 360 | <a href="/long/tree/main" tabindex="-1"> | |
| 347 | 361 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commits-root.html +111 -1
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -106,6 +110,8 @@ | |||
| 106 | 110 | <a href="/long/commit/SHA40">commit number 51</a> | |
| 107 | 111 | <span class="pill">main</span> | |
| 108 | 112 | </td> | |
| 113 | + | <td class="c-ck"> | |
| 114 | + | </td> | |
| 109 | 115 | <td class="c-who dim">T</td> | |
| 110 | 116 | <td class="c-when nowrap dim"> | |
| 111 | 117 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -118,6 +124,8 @@ | |||
| 118 | 124 | <td class="c-subject"> | |
| 119 | 125 | <a href="/long/commit/SHA40">commit number 50</a> | |
| 120 | 126 | </td> | |
| 127 | + | <td class="c-ck"> | |
| 128 | + | </td> | |
| 121 | 129 | <td class="c-who dim">T</td> | |
| 122 | 130 | <td class="c-when nowrap dim"> | |
| 123 | 131 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -139,6 +147,8 @@ | |||
| 139 | 147 | <td class="c-subject"> | |
| 140 | 148 | <a href="/long/commit/SHA40">commit number 49</a> | |
| 141 | 149 | </td> | |
| 150 | + | <td class="c-ck"> | |
| 151 | + | </td> | |
| 142 | 152 | <td class="c-who dim">T</td> | |
| 143 | 153 | <td class="c-when nowrap dim"> | |
| 144 | 154 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -151,6 +161,8 @@ | |||
| 151 | 161 | <td class="c-subject"> | |
| 152 | 162 | <a href="/long/commit/SHA40">commit number 48</a> | |
| 153 | 163 | </td> | |
| 164 | + | <td class="c-ck"> | |
| 165 | + | </td> | |
| 154 | 166 | <td class="c-who dim">T</td> | |
| 155 | 167 | <td class="c-when nowrap dim"> | |
| 156 | 168 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -172,6 +184,8 @@ | |||
| 172 | 184 | <td class="c-subject"> | |
| 173 | 185 | <a href="/long/commit/SHA40">commit number 47</a> | |
| 174 | 186 | </td> | |
| 187 | + | <td class="c-ck"> | |
| 188 | + | </td> | |
| 175 | 189 | <td class="c-who dim">T</td> | |
| 176 | 190 | <td class="c-when nowrap dim"> | |
| 177 | 191 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -184,6 +198,8 @@ | |||
| 184 | 198 | <td class="c-subject"> | |
| 185 | 199 | <a href="/long/commit/SHA40">commit number 46</a> | |
| 186 | 200 | </td> | |
| 201 | + | <td class="c-ck"> | |
| 202 | + | </td> | |
| 187 | 203 | <td class="c-who dim">T</td> | |
| 188 | 204 | <td class="c-when nowrap dim"> | |
| 189 | 205 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -205,6 +221,8 @@ | |||
| 205 | 221 | <td class="c-subject"> | |
| 206 | 222 | <a href="/long/commit/SHA40">commit number 45</a> | |
| 207 | 223 | </td> | |
| 224 | + | <td class="c-ck"> | |
| 225 | + | </td> | |
| 208 | 226 | <td class="c-who dim">T</td> | |
| 209 | 227 | <td class="c-when nowrap dim"> | |
| 210 | 228 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -217,6 +235,8 @@ | |||
| 217 | 235 | <td class="c-subject"> | |
| 218 | 236 | <a href="/long/commit/SHA40">commit number 44</a> | |
| 219 | 237 | </td> | |
| 238 | + | <td class="c-ck"> | |
| 239 | + | </td> | |
| 220 | 240 | <td class="c-who dim">T</td> | |
| 221 | 241 | <td class="c-when nowrap dim"> | |
| 222 | 242 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -238,6 +258,8 @@ | |||
| 238 | 258 | <td class="c-subject"> | |
| 239 | 259 | <a href="/long/commit/SHA40">commit number 43</a> | |
| 240 | 260 | </td> | |
| 261 | + | <td class="c-ck"> | |
| 262 | + | </td> | |
| 241 | 263 | <td class="c-who dim">T</td> | |
| 242 | 264 | <td class="c-when nowrap dim"> | |
| 243 | 265 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -250,6 +272,8 @@ | |||
| 250 | 272 | <td class="c-subject"> | |
| 251 | 273 | <a href="/long/commit/SHA40">commit number 42</a> | |
| 252 | 274 | </td> | |
| 275 | + | <td class="c-ck"> | |
| 276 | + | </td> | |
| 253 | 277 | <td class="c-who dim">T</td> | |
| 254 | 278 | <td class="c-when nowrap dim"> | |
| 255 | 279 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -271,6 +295,8 @@ | |||
| 271 | 295 | <td class="c-subject"> | |
| 272 | 296 | <a href="/long/commit/SHA40">commit number 41</a> | |
| 273 | 297 | </td> | |
| 298 | + | <td class="c-ck"> | |
| 299 | + | </td> | |
| 274 | 300 | <td class="c-who dim">T</td> | |
| 275 | 301 | <td class="c-when nowrap dim"> | |
| 276 | 302 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -283,6 +309,8 @@ | |||
| 283 | 309 | <td class="c-subject"> | |
| 284 | 310 | <a href="/long/commit/SHA40">commit number 40</a> | |
| 285 | 311 | </td> | |
| 312 | + | <td class="c-ck"> | |
| 313 | + | </td> | |
| 286 | 314 | <td class="c-who dim">T</td> | |
| 287 | 315 | <td class="c-when nowrap dim"> | |
| 288 | 316 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -304,6 +332,8 @@ | |||
| 304 | 332 | <td class="c-subject"> | |
| 305 | 333 | <a href="/long/commit/SHA40">commit number 39</a> | |
| 306 | 334 | </td> | |
| 335 | + | <td class="c-ck"> | |
| 336 | + | </td> | |
| 307 | 337 | <td class="c-who dim">T</td> | |
| 308 | 338 | <td class="c-when nowrap dim"> | |
| 309 | 339 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -316,6 +346,8 @@ | |||
| 316 | 346 | <td class="c-subject"> | |
| 317 | 347 | <a href="/long/commit/SHA40">commit number 38</a> | |
| 318 | 348 | </td> | |
| 349 | + | <td class="c-ck"> | |
| 350 | + | </td> | |
| 319 | 351 | <td class="c-who dim">T</td> | |
| 320 | 352 | <td class="c-when nowrap dim"> | |
| 321 | 353 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -337,6 +369,8 @@ | |||
| 337 | 369 | <td class="c-subject"> | |
| 338 | 370 | <a href="/long/commit/SHA40">commit number 37</a> | |
| 339 | 371 | </td> | |
| 372 | + | <td class="c-ck"> | |
| 373 | + | </td> | |
| 340 | 374 | <td class="c-who dim">T</td> | |
| 341 | 375 | <td class="c-when nowrap dim"> | |
| 342 | 376 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -349,6 +383,8 @@ | |||
| 349 | 383 | <td class="c-subject"> | |
| 350 | 384 | <a href="/long/commit/SHA40">commit number 36</a> | |
| 351 | 385 | </td> | |
| 386 | + | <td class="c-ck"> | |
| 387 | + | </td> | |
| 352 | 388 | <td class="c-who dim">T</td> | |
| 353 | 389 | <td class="c-when nowrap dim"> | |
| 354 | 390 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -370,6 +406,8 @@ | |||
| 370 | 406 | <td class="c-subject"> | |
| 371 | 407 | <a href="/long/commit/SHA40">commit number 35</a> | |
| 372 | 408 | </td> | |
| 409 | + | <td class="c-ck"> | |
| 410 | + | </td> | |
| 373 | 411 | <td class="c-who dim">T</td> | |
| 374 | 412 | <td class="c-when nowrap dim"> | |
| 375 | 413 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -382,6 +420,8 @@ | |||
| 382 | 420 | <td class="c-subject"> | |
| 383 | 421 | <a href="/long/commit/SHA40">commit number 34</a> | |
| 384 | 422 | </td> | |
| 423 | + | <td class="c-ck"> | |
| 424 | + | </td> | |
| 385 | 425 | <td class="c-who dim">T</td> | |
| 386 | 426 | <td class="c-when nowrap dim"> | |
| 387 | 427 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -403,6 +443,8 @@ | |||
| 403 | 443 | <td class="c-subject"> | |
| 404 | 444 | <a href="/long/commit/SHA40">commit number 33</a> | |
| 405 | 445 | </td> | |
| 446 | + | <td class="c-ck"> | |
| 447 | + | </td> | |
| 406 | 448 | <td class="c-who dim">T</td> | |
| 407 | 449 | <td class="c-when nowrap dim"> | |
| 408 | 450 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -415,6 +457,8 @@ | |||
| 415 | 457 | <td class="c-subject"> | |
| 416 | 458 | <a href="/long/commit/SHA40">commit number 32</a> | |
| 417 | 459 | </td> | |
| 460 | + | <td class="c-ck"> | |
| 461 | + | </td> | |
| 418 | 462 | <td class="c-who dim">T</td> | |
| 419 | 463 | <td class="c-when nowrap dim"> | |
| 420 | 464 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -436,6 +480,8 @@ | |||
| 436 | 480 | <td class="c-subject"> | |
| 437 | 481 | <a href="/long/commit/SHA40">commit number 31</a> | |
| 438 | 482 | </td> | |
| 483 | + | <td class="c-ck"> | |
| 484 | + | </td> | |
| 439 | 485 | <td class="c-who dim">T</td> | |
| 440 | 486 | <td class="c-when nowrap dim"> | |
| 441 | 487 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -448,6 +494,8 @@ | |||
| 448 | 494 | <td class="c-subject"> | |
| 449 | 495 | <a href="/long/commit/SHA40">commit number 30</a> | |
| 450 | 496 | </td> | |
| 497 | + | <td class="c-ck"> | |
| 498 | + | </td> | |
| 451 | 499 | <td class="c-who dim">T</td> | |
| 452 | 500 | <td class="c-when nowrap dim"> | |
| 453 | 501 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -469,6 +517,8 @@ | |||
| 469 | 517 | <td class="c-subject"> | |
| 470 | 518 | <a href="/long/commit/SHA40">commit number 29</a> | |
| 471 | 519 | </td> | |
| 520 | + | <td class="c-ck"> | |
| 521 | + | </td> | |
| 472 | 522 | <td class="c-who dim">T</td> | |
| 473 | 523 | <td class="c-when nowrap dim"> | |
| 474 | 524 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -481,6 +531,8 @@ | |||
| 481 | 531 | <td class="c-subject"> | |
| 482 | 532 | <a href="/long/commit/SHA40">commit number 28</a> | |
| 483 | 533 | </td> | |
| 534 | + | <td class="c-ck"> | |
| 535 | + | </td> | |
| 484 | 536 | <td class="c-who dim">T</td> | |
| 485 | 537 | <td class="c-when nowrap dim"> | |
| 486 | 538 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -502,6 +554,8 @@ | |||
| 502 | 554 | <td class="c-subject"> | |
| 503 | 555 | <a href="/long/commit/SHA40">commit number 27</a> | |
| 504 | 556 | </td> | |
| 557 | + | <td class="c-ck"> | |
| 558 | + | </td> | |
| 505 | 559 | <td class="c-who dim">T</td> | |
| 506 | 560 | <td class="c-when nowrap dim"> | |
| 507 | 561 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -514,6 +568,8 @@ | |||
| 514 | 568 | <td class="c-subject"> | |
| 515 | 569 | <a href="/long/commit/SHA40">commit number 26</a> | |
| 516 | 570 | </td> | |
| 571 | + | <td class="c-ck"> | |
| 572 | + | </td> | |
| 517 | 573 | <td class="c-who dim">T</td> | |
| 518 | 574 | <td class="c-when nowrap dim"> | |
| 519 | 575 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -535,6 +591,8 @@ | |||
| 535 | 591 | <td class="c-subject"> | |
| 536 | 592 | <a href="/long/commit/SHA40">commit number 25</a> | |
| 537 | 593 | </td> | |
| 594 | + | <td class="c-ck"> | |
| 595 | + | </td> | |
| 538 | 596 | <td class="c-who dim">T</td> | |
| 539 | 597 | <td class="c-when nowrap dim"> | |
| 540 | 598 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -547,6 +605,8 @@ | |||
| 547 | 605 | <td class="c-subject"> | |
| 548 | 606 | <a href="/long/commit/SHA40">commit number 24</a> | |
| 549 | 607 | </td> | |
| 608 | + | <td class="c-ck"> | |
| 609 | + | </td> | |
| 550 | 610 | <td class="c-who dim">T</td> | |
| 551 | 611 | <td class="c-when nowrap dim"> | |
| 552 | 612 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -568,6 +628,8 @@ | |||
| 568 | 628 | <td class="c-subject"> | |
| 569 | 629 | <a href="/long/commit/SHA40">commit number 23</a> | |
| 570 | 630 | </td> | |
| 631 | + | <td class="c-ck"> | |
| 632 | + | </td> | |
| 571 | 633 | <td class="c-who dim">T</td> | |
| 572 | 634 | <td class="c-when nowrap dim"> | |
| 573 | 635 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -580,6 +642,8 @@ | |||
| 580 | 642 | <td class="c-subject"> | |
| 581 | 643 | <a href="/long/commit/SHA40">commit number 22</a> | |
| 582 | 644 | </td> | |
| 645 | + | <td class="c-ck"> | |
| 646 | + | </td> | |
| 583 | 647 | <td class="c-who dim">T</td> | |
| 584 | 648 | <td class="c-when nowrap dim"> | |
| 585 | 649 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -601,6 +665,8 @@ | |||
| 601 | 665 | <td class="c-subject"> | |
| 602 | 666 | <a href="/long/commit/SHA40">commit number 21</a> | |
| 603 | 667 | </td> | |
| 668 | + | <td class="c-ck"> | |
| 669 | + | </td> | |
| 604 | 670 | <td class="c-who dim">T</td> | |
| 605 | 671 | <td class="c-when nowrap dim"> | |
| 606 | 672 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -613,6 +679,8 @@ | |||
| 613 | 679 | <td class="c-subject"> | |
| 614 | 680 | <a href="/long/commit/SHA40">commit number 20</a> | |
| 615 | 681 | </td> | |
| 682 | + | <td class="c-ck"> | |
| 683 | + | </td> | |
| 616 | 684 | <td class="c-who dim">T</td> | |
| 617 | 685 | <td class="c-when nowrap dim"> | |
| 618 | 686 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -634,6 +702,8 @@ | |||
| 634 | 702 | <td class="c-subject"> | |
| 635 | 703 | <a href="/long/commit/SHA40">commit number 19</a> | |
| 636 | 704 | </td> | |
| 705 | + | <td class="c-ck"> | |
| 706 | + | </td> | |
| 637 | 707 | <td class="c-who dim">T</td> | |
| 638 | 708 | <td class="c-when nowrap dim"> | |
| 639 | 709 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -646,6 +716,8 @@ | |||
| 646 | 716 | <td class="c-subject"> | |
| 647 | 717 | <a href="/long/commit/SHA40">commit number 18</a> | |
| 648 | 718 | </td> | |
| 719 | + | <td class="c-ck"> | |
| 720 | + | </td> | |
| 649 | 721 | <td class="c-who dim">T</td> | |
| 650 | 722 | <td class="c-when nowrap dim"> | |
| 651 | 723 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -667,6 +739,8 @@ | |||
| 667 | 739 | <td class="c-subject"> | |
| 668 | 740 | <a href="/long/commit/SHA40">commit number 17</a> | |
| 669 | 741 | </td> | |
| 742 | + | <td class="c-ck"> | |
| 743 | + | </td> | |
| 670 | 744 | <td class="c-who dim">T</td> | |
| 671 | 745 | <td class="c-when nowrap dim"> | |
| 672 | 746 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -679,6 +753,8 @@ | |||
| 679 | 753 | <td class="c-subject"> | |
| 680 | 754 | <a href="/long/commit/SHA40">commit number 16</a> | |
| 681 | 755 | </td> | |
| 756 | + | <td class="c-ck"> | |
| 757 | + | </td> | |
| 682 | 758 | <td class="c-who dim">T</td> | |
| 683 | 759 | <td class="c-when nowrap dim"> | |
| 684 | 760 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -700,6 +776,8 @@ | |||
| 700 | 776 | <td class="c-subject"> | |
| 701 | 777 | <a href="/long/commit/SHA40">commit number 15</a> | |
| 702 | 778 | </td> | |
| 779 | + | <td class="c-ck"> | |
| 780 | + | </td> | |
| 703 | 781 | <td class="c-who dim">T</td> | |
| 704 | 782 | <td class="c-when nowrap dim"> | |
| 705 | 783 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -712,6 +790,8 @@ | |||
| 712 | 790 | <td class="c-subject"> | |
| 713 | 791 | <a href="/long/commit/SHA40">commit number 14</a> | |
| 714 | 792 | </td> | |
| 793 | + | <td class="c-ck"> | |
| 794 | + | </td> | |
| 715 | 795 | <td class="c-who dim">T</td> | |
| 716 | 796 | <td class="c-when nowrap dim"> | |
| 717 | 797 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -733,6 +813,8 @@ | |||
| 733 | 813 | <td class="c-subject"> | |
| 734 | 814 | <a href="/long/commit/SHA40">commit number 13</a> | |
| 735 | 815 | </td> | |
| 816 | + | <td class="c-ck"> | |
| 817 | + | </td> | |
| 736 | 818 | <td class="c-who dim">T</td> | |
| 737 | 819 | <td class="c-when nowrap dim"> | |
| 738 | 820 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -745,6 +827,8 @@ | |||
| 745 | 827 | <td class="c-subject"> | |
| 746 | 828 | <a href="/long/commit/SHA40">commit number 12</a> | |
| 747 | 829 | </td> | |
| 830 | + | <td class="c-ck"> | |
| 831 | + | </td> | |
| 748 | 832 | <td class="c-who dim">T</td> | |
| 749 | 833 | <td class="c-when nowrap dim"> | |
| 750 | 834 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -766,6 +850,8 @@ | |||
| 766 | 850 | <td class="c-subject"> | |
| 767 | 851 | <a href="/long/commit/SHA40">commit number 11</a> | |
| 768 | 852 | </td> | |
| 853 | + | <td class="c-ck"> | |
| 854 | + | </td> | |
| 769 | 855 | <td class="c-who dim">T</td> | |
| 770 | 856 | <td class="c-when nowrap dim"> | |
| 771 | 857 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -778,6 +864,8 @@ | |||
| 778 | 864 | <td class="c-subject"> | |
| 779 | 865 | <a href="/long/commit/SHA40">commit number 10</a> | |
| 780 | 866 | </td> | |
| 867 | + | <td class="c-ck"> | |
| 868 | + | </td> | |
| 781 | 869 | <td class="c-who dim">T</td> | |
| 782 | 870 | <td class="c-when nowrap dim"> | |
| 783 | 871 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -799,6 +887,8 @@ | |||
| 799 | 887 | <td class="c-subject"> | |
| 800 | 888 | <a href="/long/commit/SHA40">commit number 9</a> | |
| 801 | 889 | </td> | |
| 890 | + | <td class="c-ck"> | |
| 891 | + | </td> | |
| 802 | 892 | <td class="c-who dim">T</td> | |
| 803 | 893 | <td class="c-when nowrap dim"> | |
| 804 | 894 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -811,6 +901,8 @@ | |||
| 811 | 901 | <td class="c-subject"> | |
| 812 | 902 | <a href="/long/commit/SHA40">commit number 8</a> | |
| 813 | 903 | </td> | |
| 904 | + | <td class="c-ck"> | |
| 905 | + | </td> | |
| 814 | 906 | <td class="c-who dim">T</td> | |
| 815 | 907 | <td class="c-when nowrap dim"> | |
| 816 | 908 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -832,6 +924,8 @@ | |||
| 832 | 924 | <td class="c-subject"> | |
| 833 | 925 | <a href="/long/commit/SHA40">commit number 7</a> | |
| 834 | 926 | </td> | |
| 927 | + | <td class="c-ck"> | |
| 928 | + | </td> | |
| 835 | 929 | <td class="c-who dim">T</td> | |
| 836 | 930 | <td class="c-when nowrap dim"> | |
| 837 | 931 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -844,6 +938,8 @@ | |||
| 844 | 938 | <td class="c-subject"> | |
| 845 | 939 | <a href="/long/commit/SHA40">commit number 6</a> | |
| 846 | 940 | </td> | |
| 941 | + | <td class="c-ck"> | |
| 942 | + | </td> | |
| 847 | 943 | <td class="c-who dim">T</td> | |
| 848 | 944 | <td class="c-when nowrap dim"> | |
| 849 | 945 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -865,6 +961,8 @@ | |||
| 865 | 961 | <td class="c-subject"> | |
| 866 | 962 | <a href="/long/commit/SHA40">commit number 5</a> | |
| 867 | 963 | </td> | |
| 964 | + | <td class="c-ck"> | |
| 965 | + | </td> | |
| 868 | 966 | <td class="c-who dim">T</td> | |
| 869 | 967 | <td class="c-when nowrap dim"> | |
| 870 | 968 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -877,6 +975,8 @@ | |||
| 877 | 975 | <td class="c-subject"> | |
| 878 | 976 | <a href="/long/commit/SHA40">commit number 4</a> | |
| 879 | 977 | </td> | |
| 978 | + | <td class="c-ck"> | |
| 979 | + | </td> | |
| 880 | 980 | <td class="c-who dim">T</td> | |
| 881 | 981 | <td class="c-when nowrap dim"> | |
| 882 | 982 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -898,6 +998,8 @@ | |||
| 898 | 998 | <td class="c-subject"> | |
| 899 | 999 | <a href="/long/commit/SHA40">commit number 3</a> | |
| 900 | 1000 | </td> | |
| 1001 | + | <td class="c-ck"> | |
| 1002 | + | </td> | |
| 901 | 1003 | <td class="c-who dim">T</td> | |
| 902 | 1004 | <td class="c-when nowrap dim"> | |
| 903 | 1005 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -910,6 +1012,8 @@ | |||
| 910 | 1012 | <td class="c-subject"> | |
| 911 | 1013 | <a href="/long/commit/SHA40">commit number 2</a> | |
| 912 | 1014 | </td> | |
| 1015 | + | <td class="c-ck"> | |
| 1016 | + | </td> | |
| 913 | 1017 | <td class="c-who dim">T</td> | |
| 914 | 1018 | <td class="c-when nowrap dim"> | |
| 915 | 1019 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -959,7 +1063,7 @@ | |||
| 959 | 1063 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 960 | 1064 | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 961 | 1065 | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 962 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 1066 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data); | |
| 963 | 1067 | gap -> ermiteLive.reload()"> | |
| 964 | 1068 | </span> | |
| 965 | 1069 | </div> | |
| @@ -1131,6 +1235,12 @@ | |||
| 1131 | 1235 | <span class="k">section</span> | |
| 1132 | 1236 | </a> | |
| 1133 | 1237 | </li> | |
| 1238 | + | <li data-k="section actions"> | |
| 1239 | + | <a href="/long/actions" tabindex="-1"> | |
| 1240 | + | <span class="lbl">actions</span> | |
| 1241 | + | <span class="k">section</span> | |
| 1242 | + | </a> | |
| 1243 | + | </li> | |
| 1134 | 1244 | <li data-k="branch main"> | |
| 1135 | 1245 | <a href="/long/tree/main" tabindex="-1"> | |
| 1136 | 1246 | <span class="lbl">main</span> | |
M internal/app/testdata/web/commits-small.html +27 -1
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -106,6 +110,8 @@ | |||
| 106 | 110 | <a href="/diffs/commit/SHA40">add a very large file</a> | |
| 107 | 111 | <span class="pill">main</span> | |
| 108 | 112 | </td> | |
| 113 | + | <td class="c-ck"> | |
| 114 | + | </td> | |
| 109 | 115 | <td class="c-who dim">T</td> | |
| 110 | 116 | <td class="c-when nowrap dim"> | |
| 111 | 117 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -127,6 +133,8 @@ | |||
| 127 | 133 | <td class="c-subject"> | |
| 128 | 134 | <a href="/diffs/commit/SHA40">add evil.go</a> | |
| 129 | 135 | </td> | |
| 136 | + | <td class="c-ck"> | |
| 137 | + | </td> | |
| 130 | 138 | <td class="c-who dim">T</td> | |
| 131 | 139 | <td class="c-when nowrap dim"> | |
| 132 | 140 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -149,6 +157,8 @@ | |||
| 149 | 157 | <a href="/diffs/commit/SHA40">merge the side branch</a> | |
| 150 | 158 | <span class="pill">merge</span> | |
| 151 | 159 | </td> | |
| 160 | + | <td class="c-ck"> | |
| 161 | + | </td> | |
| 152 | 162 | <td class="c-who dim">T</td> | |
| 153 | 163 | <td class="c-when nowrap dim"> | |
| 154 | 164 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -171,6 +181,8 @@ | |||
| 171 | 181 | <a href="/diffs/commit/SHA40">work on the side</a> | |
| 172 | 182 | <span class="pill">side</span> | |
| 173 | 183 | </td> | |
| 184 | + | <td class="c-ck"> | |
| 185 | + | </td> | |
| 174 | 186 | <td class="c-who dim">T</td> | |
| 175 | 187 | <td class="c-when nowrap dim"> | |
| 176 | 188 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -192,6 +204,8 @@ | |||
| 192 | 204 | <td class="c-subject"> | |
| 193 | 205 | <a href="/diffs/commit/SHA40">change a binary file</a> | |
| 194 | 206 | </td> | |
| 207 | + | <td class="c-ck"> | |
| 208 | + | </td> | |
| 195 | 209 | <td class="c-who dim">T</td> | |
| 196 | 210 | <td class="c-when nowrap dim"> | |
| 197 | 211 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -213,6 +227,8 @@ | |||
| 213 | 227 | <td class="c-subject"> | |
| 214 | 228 | <a href="/diffs/commit/SHA40">rename a file and delete another</a> | |
| 215 | 229 | </td> | |
| 230 | + | <td class="c-ck"> | |
| 231 | + | </td> | |
| 216 | 232 | <td class="c-who dim">T</td> | |
| 217 | 233 | <td class="c-when nowrap dim"> | |
| 218 | 234 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -236,6 +252,8 @@ | |||
| 236 | 252 | <span class="pill tag"> | |
| 237 | 253 | <span class="sr">tag </span>v1.0.0</span> | |
| 238 | 254 | </td> | |
| 255 | + | <td class="c-ck"> | |
| 256 | + | </td> | |
| 239 | 257 | <td class="c-who dim">T</td> | |
| 240 | 258 | <td class="c-when nowrap dim"> | |
| 241 | 259 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -257,6 +275,8 @@ | |||
| 257 | 275 | <td class="c-subject"> | |
| 258 | 276 | <a href="/diffs/commit/SHA40">root: the first files</a> | |
| 259 | 277 | </td> | |
| 278 | + | <td class="c-ck"> | |
| 279 | + | </td> | |
| 260 | 280 | <td class="c-who dim">T</td> | |
| 261 | 281 | <td class="c-when nowrap dim"> | |
| 262 | 282 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -304,7 +324,7 @@ | |||
| 304 | 324 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 305 | 325 | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 306 | 326 | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 307 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 327 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data); | |
| 308 | 328 | gap -> ermiteLive.reload()"> | |
| 309 | 329 | </span> | |
| 310 | 330 | </div> | |
| @@ -484,6 +504,12 @@ | |||
| 484 | 504 | <span class="k">section</span> | |
| 485 | 505 | </a> | |
| 486 | 506 | </li> | |
| 507 | + | <li data-k="section actions"> | |
| 508 | + | <a href="/diffs/actions" tabindex="-1"> | |
| 509 | + | <span class="lbl">actions</span> | |
| 510 | + | <span class="k">section</span> | |
| 511 | + | </a> | |
| 512 | + | </li> | |
| 487 | 513 | <li data-k="branch main"> | |
| 488 | 514 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 489 | 515 | <span class="lbl">main</span> | |
M internal/app/testdata/web/home.html +1 -1
| @@ -113,7 +113,7 @@ | |||
| 113 | 113 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 114 | 114 | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 115 | 115 | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 116 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 116 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data); | |
| 117 | 117 | gap -> ermiteLive.reload()"> | |
| 118 | 118 | </span> | |
| 119 | 119 | </div> | |
M internal/app/testdata/web/refs.html +11 -1
| @@ -77,6 +77,10 @@ | |||
| 77 | 77 | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 78 | 78 | </a> | |
| 79 | 79 | </li> | |
| 80 | + | <li> | |
| 81 | + | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 82 | + | </a> | |
| 83 | + | </li> | |
| 80 | 84 | </ul> | |
| 81 | 85 | </nav> | |
| 82 | 86 | </header> | |
| @@ -196,7 +200,7 @@ | |||
| 196 | 200 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 197 | 201 | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 198 | 202 | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 199 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 203 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data); | |
| 200 | 204 | gap -> ermiteLive.reload()"> | |
| 201 | 205 | </span> | |
| 202 | 206 | </div> | |
| @@ -380,6 +384,12 @@ | |||
| 380 | 384 | <span class="k">section</span> | |
| 381 | 385 | </a> | |
| 382 | 386 | </li> | |
| 387 | + | <li data-k="section actions"> | |
| 388 | + | <a href="/demo/actions" tabindex="-1"> | |
| 389 | + | <span class="lbl">actions</span> | |
| 390 | + | <span class="k">section</span> | |
| 391 | + | </a> | |
| 392 | + | </li> | |
| 383 | 393 | <li data-k="branch feature/x"> | |
| 384 | 394 | <a href="/demo/tree/feature/x" tabindex="-1"> | |
| 385 | 395 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/summary.html +15 -1
| @@ -74,6 +74,10 @@ | |||
| 74 | 74 | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 75 | 75 | </a> | |
| 76 | 76 | </li> | |
| 77 | + | <li> | |
| 78 | + | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 79 | + | </a> | |
| 80 | + | </li> | |
| 77 | 81 | </ul> | |
| 78 | 82 | </nav> | |
| 79 | 83 | </header> | |
| @@ -166,6 +170,8 @@ | |||
| 166 | 170 | <span class="pill tag"> | |
| 167 | 171 | <span class="sr">tag </span>v0.2.0</span> | |
| 168 | 172 | </td> | |
| 173 | + | <td class="c-ck"> | |
| 174 | + | </td> | |
| 169 | 175 | <td class="c-who dim">T</td> | |
| 170 | 176 | <td class="c-when nowrap dim"> | |
| 171 | 177 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -180,6 +186,8 @@ | |||
| 180 | 186 | <span class="pill tag"> | |
| 181 | 187 | <span class="sr">tag </span>v0.1.0</span> | |
| 182 | 188 | </td> | |
| 189 | + | <td class="c-ck"> | |
| 190 | + | </td> | |
| 183 | 191 | <td class="c-who dim">T</td> | |
| 184 | 192 | <td class="c-when nowrap dim"> | |
| 185 | 193 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| @@ -215,7 +223,7 @@ | |||
| 215 | 223 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 216 | 224 | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 217 | 225 | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 218 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 226 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data); | |
| 219 | 227 | gap -> ermiteLive.reload()"> | |
| 220 | 228 | </span> | |
| 221 | 229 | </div> | |
| @@ -399,6 +407,12 @@ | |||
| 399 | 407 | <span class="k">section</span> | |
| 400 | 408 | </a> | |
| 401 | 409 | </li> | |
| 410 | + | <li data-k="section actions"> | |
| 411 | + | <a href="/demo/actions" tabindex="-1"> | |
| 412 | + | <span class="lbl">actions</span> | |
| 413 | + | <span class="k">section</span> | |
| 414 | + | </a> | |
| 415 | + | </li> | |
| 402 | 416 | <li data-k="branch feature/x"> | |
| 403 | 417 | <a href="/demo/tree/feature/x" tabindex="-1"> | |
| 404 | 418 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/tree-nested.html +10 -0
| @@ -86,6 +86,10 @@ | |||
| 86 | 86 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 87 | 87 | </a> | |
| 88 | 88 | </li> | |
| 89 | + | <li> | |
| 90 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 91 | + | </a> | |
| 92 | + | </li> | |
| 89 | 93 | </ul> | |
| 90 | 94 | </nav> | |
| 91 | 95 | </header> | |
| @@ -328,6 +332,12 @@ | |||
| 328 | 332 | <span class="k">section</span> | |
| 329 | 333 | </a> | |
| 330 | 334 | </li> | |
| 335 | + | <li data-k="section actions"> | |
| 336 | + | <a href="/files/actions" tabindex="-1"> | |
| 337 | + | <span class="lbl">actions</span> | |
| 338 | + | <span class="k">section</span> | |
| 339 | + | </a> | |
| 340 | + | </li> | |
| 331 | 341 | <li data-k="branch feature/x"> | |
| 332 | 342 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 333 | 343 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/tree-root.html +10 -0
| @@ -80,6 +80,10 @@ | |||
| 80 | 80 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 81 | 81 | </a> | |
| 82 | 82 | </li> | |
| 83 | + | <li> | |
| 84 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 85 | + | </a> | |
| 86 | + | </li> | |
| 83 | 87 | </ul> | |
| 84 | 88 | </nav> | |
| 85 | 89 | </header> | |
| @@ -555,6 +559,12 @@ | |||
| 555 | 559 | <span class="k">section</span> | |
| 556 | 560 | </a> | |
| 557 | 561 | </li> | |
| 562 | + | <li data-k="section actions"> | |
| 563 | + | <a href="/files/actions" tabindex="-1"> | |
| 564 | + | <span class="lbl">actions</span> | |
| 565 | + | <span class="k">section</span> | |
| 566 | + | </a> | |
| 567 | + | </li> | |
| 558 | 568 | <li data-k="branch feature/x"> | |
| 559 | 569 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 560 | 570 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/tree-slash-ref.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -325,6 +329,12 @@ | |||
| 325 | 329 | <span class="k">section</span> | |
| 326 | 330 | </a> | |
| 327 | 331 | </li> | |
| 332 | + | <li data-k="section actions"> | |
| 333 | + | <a href="/files/actions" tabindex="-1"> | |
| 334 | + | <span class="lbl">actions</span> | |
| 335 | + | <span class="k">section</span> | |
| 336 | + | </a> | |
| 337 | + | </li> | |
| 328 | 338 | <li data-k="branch feature/x"> | |
| 329 | 339 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 330 | 340 | <span class="lbl">feature/x</span> | |
M internal/app/testdata/web/tree-subdir.html +10 -0
| @@ -83,6 +83,10 @@ | |||
| 83 | 83 | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> | |
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | + | <li> | |
| 87 | + | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 86 | 90 | </ul> | |
| 87 | 91 | </nav> | |
| 88 | 92 | </header> | |
| @@ -350,6 +354,12 @@ | |||
| 350 | 354 | <span class="k">section</span> | |
| 351 | 355 | </a> | |
| 352 | 356 | </li> | |
| 357 | + | <li data-k="section actions"> | |
| 358 | + | <a href="/files/actions" tabindex="-1"> | |
| 359 | + | <span class="lbl">actions</span> | |
| 360 | + | <span class="k">section</span> | |
| 361 | + | </a> | |
| 362 | + | </li> | |
| 353 | 363 | <li data-k="branch feature/x"> | |
| 354 | 364 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 355 | 365 | <span class="lbl">feature/x</span> | |
M internal/app/web.go +1 -0
| @@ -33,6 +33,7 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han | |||
| 33 | 33 | Repos: f.Repos, | |
| 34 | 34 | Browse: f.Browse, | |
| 35 | 35 | MergeRequests: f.MergeRequests, | |
| 36 | + | Runs: f.CI, | |
| 36 | 37 | Events: f.EventLog, | |
| 37 | 38 | PublicURL: cfg.PublicURL, | |
| 38 | 39 | SourceURL: cfg.SourceURL, | |