@@ -6,20 +6,81 @@ import ( |
| 6 | "errors" | 6 | "errors" |
| 7 | "html/template" | 7 | "html/template" |
| 8 | "net/http" | 8 | "net/http" |
| | 9 | + "net/url" |
| | 10 | + "path" |
| 9 | "strconv" | 11 | "strconv" |
| | 12 | + "strings" |
| 10 | "time" | 13 | "time" |
| 11 | | 14 | |
| 12 | "github.com/go-chi/chi/v5" | 15 | "github.com/go-chi/chi/v5" |
| 13 | "github.com/jackc/pgx/v5" | 16 | "github.com/jackc/pgx/v5" |
| | 17 | + "github.com/jackc/pgx/v5/pgtype" |
| 14 | | 18 | |
| | 19 | + actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc" |
| 15 | "github.com/tenseleyFlow/shithub/internal/auth/policy" | 20 | "github.com/tenseleyFlow/shithub/internal/auth/policy" |
| 16 | checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc" | 21 | checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc" |
| 17 | "github.com/tenseleyFlow/shithub/internal/web/middleware" | 22 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 18 | ) | 23 | ) |
| 19 | | 24 | |
| | 25 | +const actionsRunsPageSize = int32(20) |
| | 26 | + |
| 20 | type actionsWorkflowView struct { | 27 | type actionsWorkflowView struct { |
| 21 | - Name string | 28 | + File string |
| 22 | - Count int | 29 | + Name string |
| | 30 | + Count int64 |
| | 31 | + Href string |
| | 32 | + Active bool |
| | 33 | +} |
| | 34 | + |
| | 35 | +type actionsListRunView struct { |
| | 36 | + ID int64 |
| | 37 | + RunIndex int64 |
| | 38 | + WorkflowFile string |
| | 39 | + WorkflowName string |
| | 40 | + Title string |
| | 41 | + HeadSha string |
| | 42 | + HeadShaShort string |
| | 43 | + HeadRef string |
| | 44 | + Event string |
| | 45 | + EventLabel string |
| | 46 | + ActorUsername string |
| | 47 | + StateText string |
| | 48 | + StateClass string |
| | 49 | + StateIcon string |
| | 50 | + CreatedAt time.Time |
| | 51 | + UpdatedAt time.Time |
| | 52 | + Duration string |
| | 53 | + Href string |
| | 54 | +} |
| | 55 | + |
| | 56 | +type actionsListFilters struct { |
| | 57 | + Workflow string |
| | 58 | + Branch string |
| | 59 | + Event string |
| | 60 | + Status string |
| | 61 | + Conclusion string |
| | 62 | + Actor string |
| | 63 | + Page int32 |
| | 64 | + HasAny bool |
| | 65 | +} |
| | 66 | + |
| | 67 | +type actionsFilterOption struct { |
| | 68 | + Value string |
| | 69 | + Label string |
| | 70 | + Selected bool |
| | 71 | +} |
| | 72 | + |
| | 73 | +type actionsPaginationView struct { |
| | 74 | + Page int32 |
| | 75 | + PageSize int32 |
| | 76 | + Total int64 |
| | 77 | + Start int64 |
| | 78 | + End int64 |
| | 79 | + HasPrev bool |
| | 80 | + HasNext bool |
| | 81 | + PrevHref string |
| | 82 | + NextHref string |
| | 83 | + ResultText string |
| 23 | } | 84 | } |
| 24 | | 85 | |
| 25 | type actionsSuiteView struct { | 86 | type actionsSuiteView struct { |
@@ -60,48 +121,417 @@ func (h *Handlers) repoTabActions(w http.ResponseWriter, r *http.Request) { |
| 60 | if !ok { | 121 | if !ok { |
| 61 | return | 122 | return |
| 62 | } | 123 | } |
| 63 | - suiteRows, err := h.cq.ListCheckSuitesForRepo(r.Context(), h.d.Pool, checksdb.ListCheckSuitesForRepoParams{ | 124 | + |
| 64 | - RepoID: row.ID, | 125 | + filters := actionsListFiltersFromRequest(r) |
| 65 | - Limit: 50, | 126 | + q := actionsdb.New() |
| 66 | - Offset: 0, | 127 | + params := workflowRunListParams(row.ID, filters) |
| 67 | - }) | 128 | + params.PageLimit = actionsRunsPageSize |
| | 129 | + params.PageOffset = (filters.Page - 1) * actionsRunsPageSize |
| | 130 | + |
| | 131 | + runs, err := q.ListWorkflowRunsForRepo(r.Context(), h.d.Pool, params) |
| 68 | if err != nil { | 132 | if err != nil { |
| 69 | - h.d.Logger.WarnContext(r.Context(), "repo actions: list suites", "error", err) | 133 | + h.d.Logger.WarnContext(r.Context(), "repo actions: list workflow runs", "repo_id", row.ID, "error", err) |
| 70 | h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") | 134 | h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") |
| 71 | return | 135 | return |
| 72 | } | 136 | } |
| 73 | - | 137 | + filteredCount, err := q.CountWorkflowRunsForRepo(r.Context(), h.d.Pool, workflowRunCountParams(row.ID, filters)) |
| 74 | - suites := make([]actionsSuiteView, 0, len(suiteRows)) | 138 | + if err != nil { |
| 75 | - workflowCounts := map[string]int{} | 139 | + h.d.Logger.WarnContext(r.Context(), "repo actions: count workflow runs", "repo_id", row.ID, "error", err) |
| 76 | - workflowOrder := []string{} | 140 | + h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") |
| 77 | - for _, suite := range suiteRows { | 141 | + return |
| 78 | - runs, err := h.cq.ListCheckRunsBySuite(r.Context(), h.d.Pool, suite.ID) | 142 | + } |
| 79 | - if err != nil { | 143 | + workflowRows, err := q.ListWorkflowRunWorkflowsForRepo(r.Context(), h.d.Pool, row.ID) |
| 80 | - h.d.Logger.WarnContext(r.Context(), "repo actions: list runs", "suite_id", suite.ID, "error", err) | 144 | + if err != nil { |
| 81 | - continue | 145 | + h.d.Logger.WarnContext(r.Context(), "repo actions: list workflows", "repo_id", row.ID, "error", err) |
| 82 | - } | 146 | + h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") |
| 83 | - if _, ok := workflowCounts[suite.AppSlug]; !ok { | 147 | + return |
| 84 | - workflowOrder = append(workflowOrder, suite.AppSlug) | | |
| 85 | - } | | |
| 86 | - workflowCounts[suite.AppSlug]++ | | |
| 87 | - suites = append(suites, actionsSuiteViewFromListRow(suite, runs)) | | |
| 88 | } | 148 | } |
| 89 | | 149 | |
| 90 | - workflows := make([]actionsWorkflowView, 0, len(workflowOrder)) | 150 | + basePath := "/" + owner.Username + "/" + row.Name + "/actions" |
| 91 | - for _, name := range workflowOrder { | 151 | + workflows, allRunCount, activeWorkflowName := actionsWorkflowViews(workflowRows, filters, basePath) |
| 92 | - workflows = append(workflows, actionsWorkflowView{Name: name, Count: workflowCounts[name]}) | 152 | + runViews := make([]actionsListRunView, 0, len(runs)) |
| | 153 | + now := time.Now() |
| | 154 | + for _, run := range runs { |
| | 155 | + runViews = append(runViews, actionsListRunViewFromRow(run, owner.Username, row.Name, now)) |
| 93 | } | 156 | } |
| 94 | | 157 | |
| 95 | data := h.repoHeaderData(r, row, owner.Username, "actions") | 158 | data := h.repoHeaderData(r, row, owner.Username, "actions") |
| 96 | data["Title"] = "Actions · " + row.Name | 159 | data["Title"] = "Actions · " + row.Name |
| 97 | - data["Suites"] = suites | 160 | + data["Runs"] = runViews |
| 98 | data["Workflows"] = workflows | 161 | data["Workflows"] = workflows |
| 99 | - data["RunCount"] = len(suites) | 162 | + data["RunCount"] = allRunCount |
| | 163 | + data["FilteredRunCount"] = filteredCount |
| | 164 | + data["ActiveWorkflowName"] = activeWorkflowName |
| | 165 | + data["Filters"] = filters |
| | 166 | + data["EventOptions"] = actionsEventOptions(filters.Event) |
| | 167 | + data["StatusOptions"] = actionsStatusOptions(filters.Status) |
| | 168 | + data["ConclusionOptions"] = actionsConclusionOptions(filters.Conclusion) |
| | 169 | + data["Pagination"] = actionsPagination(basePath, filters, filteredCount, int64(len(runViews))) |
| 100 | if err := h.d.Render.RenderPage(w, r, "repo/actions", data); err != nil { | 170 | if err := h.d.Render.RenderPage(w, r, "repo/actions", data); err != nil { |
| 101 | h.d.Logger.ErrorContext(r.Context(), "repo actions render", "error", err) | 171 | h.d.Logger.ErrorContext(r.Context(), "repo actions render", "error", err) |
| 102 | } | 172 | } |
| 103 | } | 173 | } |
| 104 | | 174 | |
| | 175 | +func actionsListFiltersFromRequest(r *http.Request) actionsListFilters { |
| | 176 | + q := r.URL.Query() |
| | 177 | + f := actionsListFilters{ |
| | 178 | + Workflow: trimFilter(q.Get("workflow"), 256), |
| | 179 | + Branch: trimFilter(q.Get("branch"), 256), |
| | 180 | + Event: validWorkflowRunEvent(q.Get("event")), |
| | 181 | + Status: validWorkflowRunStatus(q.Get("status")), |
| | 182 | + Conclusion: validWorkflowRunConclusion(q.Get("conclusion")), |
| | 183 | + Actor: trimFilter(q.Get("actor"), 39), |
| | 184 | + Page: parseActionsPage(q.Get("page")), |
| | 185 | + } |
| | 186 | + f.HasAny = f.Workflow != "" || f.Branch != "" || f.Event != "" || f.Status != "" || f.Conclusion != "" || f.Actor != "" |
| | 187 | + return f |
| | 188 | +} |
| | 189 | + |
| | 190 | +func trimFilter(v string, max int) string { |
| | 191 | + v = strings.TrimSpace(v) |
| | 192 | + if len(v) > max { |
| | 193 | + return v[:max] |
| | 194 | + } |
| | 195 | + return v |
| | 196 | +} |
| | 197 | + |
| | 198 | +func parseActionsPage(v string) int32 { |
| | 199 | + page, err := strconv.ParseInt(strings.TrimSpace(v), 10, 32) |
| | 200 | + if err != nil || page < 1 { |
| | 201 | + return 1 |
| | 202 | + } |
| | 203 | + if page > 100000 { |
| | 204 | + return 100000 |
| | 205 | + } |
| | 206 | + return int32(page) |
| | 207 | +} |
| | 208 | + |
| | 209 | +func workflowRunListParams(repoID int64, filters actionsListFilters) actionsdb.ListWorkflowRunsForRepoParams { |
| | 210 | + return actionsdb.ListWorkflowRunsForRepoParams{ |
| | 211 | + RepoID: repoID, |
| | 212 | + WorkflowFile: nullableText(filters.Workflow), |
| | 213 | + HeadRef: nullableText(filters.Branch), |
| | 214 | + Event: nullableWorkflowRunEvent(filters.Event), |
| | 215 | + Status: nullableWorkflowRunStatus(filters.Status), |
| | 216 | + Conclusion: nullableWorkflowRunConclusion(filters.Conclusion), |
| | 217 | + ActorUsername: nullableText(filters.Actor), |
| | 218 | + } |
| | 219 | +} |
| | 220 | + |
| | 221 | +func workflowRunCountParams(repoID int64, filters actionsListFilters) actionsdb.CountWorkflowRunsForRepoParams { |
| | 222 | + return actionsdb.CountWorkflowRunsForRepoParams{ |
| | 223 | + RepoID: repoID, |
| | 224 | + WorkflowFile: nullableText(filters.Workflow), |
| | 225 | + HeadRef: nullableText(filters.Branch), |
| | 226 | + Event: nullableWorkflowRunEvent(filters.Event), |
| | 227 | + Status: nullableWorkflowRunStatus(filters.Status), |
| | 228 | + Conclusion: nullableWorkflowRunConclusion(filters.Conclusion), |
| | 229 | + ActorUsername: nullableText(filters.Actor), |
| | 230 | + } |
| | 231 | +} |
| | 232 | + |
| | 233 | +func nullableText(v string) pgtype.Text { |
| | 234 | + if v == "" { |
| | 235 | + return pgtype.Text{} |
| | 236 | + } |
| | 237 | + return pgtype.Text{String: v, Valid: true} |
| | 238 | +} |
| | 239 | + |
| | 240 | +func nullableWorkflowRunEvent(v string) actionsdb.NullWorkflowRunEvent { |
| | 241 | + if v == "" { |
| | 242 | + return actionsdb.NullWorkflowRunEvent{} |
| | 243 | + } |
| | 244 | + return actionsdb.NullWorkflowRunEvent{WorkflowRunEvent: actionsdb.WorkflowRunEvent(v), Valid: true} |
| | 245 | +} |
| | 246 | + |
| | 247 | +func nullableWorkflowRunStatus(v string) actionsdb.NullWorkflowRunStatus { |
| | 248 | + if v == "" { |
| | 249 | + return actionsdb.NullWorkflowRunStatus{} |
| | 250 | + } |
| | 251 | + return actionsdb.NullWorkflowRunStatus{WorkflowRunStatus: actionsdb.WorkflowRunStatus(v), Valid: true} |
| | 252 | +} |
| | 253 | + |
| | 254 | +func nullableWorkflowRunConclusion(v string) actionsdb.NullCheckConclusion { |
| | 255 | + if v == "" { |
| | 256 | + return actionsdb.NullCheckConclusion{} |
| | 257 | + } |
| | 258 | + return actionsdb.NullCheckConclusion{CheckConclusion: actionsdb.CheckConclusion(v), Valid: true} |
| | 259 | +} |
| | 260 | + |
| | 261 | +func actionsWorkflowViews(rows []actionsdb.ListWorkflowRunWorkflowsForRepoRow, filters actionsListFilters, basePath string) ([]actionsWorkflowView, int64, string) { |
| | 262 | + params := actionsFilterParams(filters) |
| | 263 | + params.Del("page") |
| | 264 | + out := make([]actionsWorkflowView, 0, len(rows)) |
| | 265 | + var total int64 |
| | 266 | + activeName := "" |
| | 267 | + for _, row := range rows { |
| | 268 | + total += row.RunCount |
| | 269 | + name := workflowDisplayName(row.WorkflowName, row.WorkflowFile) |
| | 270 | + p := cloneValues(params) |
| | 271 | + p.Set("workflow", row.WorkflowFile) |
| | 272 | + active := filters.Workflow == row.WorkflowFile |
| | 273 | + if active { |
| | 274 | + activeName = name |
| | 275 | + } |
| | 276 | + out = append(out, actionsWorkflowView{ |
| | 277 | + File: row.WorkflowFile, |
| | 278 | + Name: name, |
| | 279 | + Count: row.RunCount, |
| | 280 | + Href: pathWithQuery(basePath, p), |
| | 281 | + Active: active, |
| | 282 | + }) |
| | 283 | + } |
| | 284 | + return out, total, activeName |
| | 285 | +} |
| | 286 | + |
| | 287 | +func actionsListRunViewFromRow(row actionsdb.ListWorkflowRunsForRepoRow, owner, repoName string, now time.Time) actionsListRunView { |
| | 288 | + stateText, stateClass, stateIcon := workflowRunState(row.Status, row.Conclusion) |
| | 289 | + title := workflowDisplayName(row.WorkflowName, row.WorkflowFile) |
| | 290 | + updatedAt := row.UpdatedAt.Time |
| | 291 | + if updatedAt.IsZero() { |
| | 292 | + updatedAt = row.CreatedAt.Time |
| | 293 | + } |
| | 294 | + return actionsListRunView{ |
| | 295 | + ID: row.ID, |
| | 296 | + RunIndex: row.RunIndex, |
| | 297 | + WorkflowFile: row.WorkflowFile, |
| | 298 | + WorkflowName: row.WorkflowName, |
| | 299 | + Title: title, |
| | 300 | + HeadSha: row.HeadSha, |
| | 301 | + HeadShaShort: shortSHA(row.HeadSha), |
| | 302 | + HeadRef: row.HeadRef, |
| | 303 | + Event: string(row.Event), |
| | 304 | + EventLabel: workflowRunEventLabel(string(row.Event)), |
| | 305 | + ActorUsername: row.ActorUsername, |
| | 306 | + StateText: stateText, |
| | 307 | + StateClass: stateClass, |
| | 308 | + StateIcon: stateIcon, |
| | 309 | + CreatedAt: row.CreatedAt.Time, |
| | 310 | + UpdatedAt: updatedAt, |
| | 311 | + Duration: workflowRunDuration(row.Status, row.StartedAt, row.CompletedAt, row.CreatedAt, updatedAt, now), |
| | 312 | + Href: "/" + owner + "/" + repoName + "/actions/runs/" + strconv.FormatInt(row.RunIndex, 10), |
| | 313 | + } |
| | 314 | +} |
| | 315 | + |
| | 316 | +func workflowDisplayName(name, file string) string { |
| | 317 | + name = strings.TrimSpace(name) |
| | 318 | + if name != "" { |
| | 319 | + return name |
| | 320 | + } |
| | 321 | + base := path.Base(file) |
| | 322 | + ext := path.Ext(base) |
| | 323 | + if ext != "" { |
| | 324 | + base = strings.TrimSuffix(base, ext) |
| | 325 | + } |
| | 326 | + if base == "." || base == "/" || base == "" { |
| | 327 | + return file |
| | 328 | + } |
| | 329 | + return base |
| | 330 | +} |
| | 331 | + |
| | 332 | +func workflowRunState(status actionsdb.WorkflowRunStatus, conclusion actionsdb.NullCheckConclusion) (string, string, string) { |
| | 333 | + switch status { |
| | 334 | + case actionsdb.WorkflowRunStatusQueued: |
| | 335 | + return "Queued", "pending", "dot-fill" |
| | 336 | + case actionsdb.WorkflowRunStatusRunning: |
| | 337 | + return "In progress", "running", "dot-fill" |
| | 338 | + case actionsdb.WorkflowRunStatusCancelled: |
| | 339 | + return "Cancelled", "neutral", "x-circle" |
| | 340 | + case actionsdb.WorkflowRunStatusCompleted: |
| | 341 | + if !conclusion.Valid { |
| | 342 | + return "Completed", "neutral", "check-circle" |
| | 343 | + } |
| | 344 | + default: |
| | 345 | + if !conclusion.Valid { |
| | 346 | + return string(status), "neutral", "dot-fill" |
| | 347 | + } |
| | 348 | + } |
| | 349 | + switch conclusion.CheckConclusion { |
| | 350 | + case actionsdb.CheckConclusionSuccess, actionsdb.CheckConclusionSkipped, actionsdb.CheckConclusionNeutral: |
| | 351 | + return "Success", "success", "check-circle-fill" |
| | 352 | + case actionsdb.CheckConclusionFailure, actionsdb.CheckConclusionTimedOut, actionsdb.CheckConclusionActionRequired: |
| | 353 | + return "Failure", "failure", "x-circle-fill" |
| | 354 | + case actionsdb.CheckConclusionCancelled, actionsdb.CheckConclusionStale: |
| | 355 | + return "Cancelled", "neutral", "x-circle" |
| | 356 | + default: |
| | 357 | + return string(conclusion.CheckConclusion), "neutral", "dot-fill" |
| | 358 | + } |
| | 359 | +} |
| | 360 | + |
| | 361 | +func workflowRunDuration(status actionsdb.WorkflowRunStatus, startedAt, completedAt, createdAt pgtype.Timestamptz, updatedAt, now time.Time) string { |
| | 362 | + if status == actionsdb.WorkflowRunStatusQueued { |
| | 363 | + return "—" |
| | 364 | + } |
| | 365 | + start := createdAt.Time |
| | 366 | + if startedAt.Valid { |
| | 367 | + start = startedAt.Time |
| | 368 | + } |
| | 369 | + end := updatedAt |
| | 370 | + if status == actionsdb.WorkflowRunStatusRunning { |
| | 371 | + end = now |
| | 372 | + } else if completedAt.Valid { |
| | 373 | + end = completedAt.Time |
| | 374 | + } |
| | 375 | + return formatDuration(end.Sub(start)) |
| | 376 | +} |
| | 377 | + |
| | 378 | +func validWorkflowRunEvent(v string) string { |
| | 379 | + switch strings.TrimSpace(v) { |
| | 380 | + case "push", "pull_request", "schedule", "workflow_dispatch": |
| | 381 | + return strings.TrimSpace(v) |
| | 382 | + default: |
| | 383 | + return "" |
| | 384 | + } |
| | 385 | +} |
| | 386 | + |
| | 387 | +func validWorkflowRunStatus(v string) string { |
| | 388 | + switch strings.TrimSpace(v) { |
| | 389 | + case "queued", "running", "completed", "cancelled": |
| | 390 | + return strings.TrimSpace(v) |
| | 391 | + default: |
| | 392 | + return "" |
| | 393 | + } |
| | 394 | +} |
| | 395 | + |
| | 396 | +func validWorkflowRunConclusion(v string) string { |
| | 397 | + switch strings.TrimSpace(v) { |
| | 398 | + case "success", "failure", "neutral", "cancelled", "skipped", "timed_out", "action_required", "stale": |
| | 399 | + return strings.TrimSpace(v) |
| | 400 | + default: |
| | 401 | + return "" |
| | 402 | + } |
| | 403 | +} |
| | 404 | + |
| | 405 | +func actionsEventOptions(selected string) []actionsFilterOption { |
| | 406 | + return selectedOptions(selected, []actionsFilterOption{ |
| | 407 | + {Value: "", Label: "Any event"}, |
| | 408 | + {Value: "push", Label: "push"}, |
| | 409 | + {Value: "pull_request", Label: "pull_request"}, |
| | 410 | + {Value: "schedule", Label: "schedule"}, |
| | 411 | + {Value: "workflow_dispatch", Label: "workflow_dispatch"}, |
| | 412 | + }) |
| | 413 | +} |
| | 414 | + |
| | 415 | +func actionsStatusOptions(selected string) []actionsFilterOption { |
| | 416 | + return selectedOptions(selected, []actionsFilterOption{ |
| | 417 | + {Value: "", Label: "Any status"}, |
| | 418 | + {Value: "queued", Label: "queued"}, |
| | 419 | + {Value: "running", Label: "running"}, |
| | 420 | + {Value: "completed", Label: "completed"}, |
| | 421 | + {Value: "cancelled", Label: "cancelled"}, |
| | 422 | + }) |
| | 423 | +} |
| | 424 | + |
| | 425 | +func actionsConclusionOptions(selected string) []actionsFilterOption { |
| | 426 | + return selectedOptions(selected, []actionsFilterOption{ |
| | 427 | + {Value: "", Label: "Any conclusion"}, |
| | 428 | + {Value: "success", Label: "success"}, |
| | 429 | + {Value: "failure", Label: "failure"}, |
| | 430 | + {Value: "neutral", Label: "neutral"}, |
| | 431 | + {Value: "cancelled", Label: "cancelled"}, |
| | 432 | + {Value: "skipped", Label: "skipped"}, |
| | 433 | + {Value: "timed_out", Label: "timed_out"}, |
| | 434 | + {Value: "action_required", Label: "action_required"}, |
| | 435 | + {Value: "stale", Label: "stale"}, |
| | 436 | + }) |
| | 437 | +} |
| | 438 | + |
| | 439 | +func selectedOptions(selected string, opts []actionsFilterOption) []actionsFilterOption { |
| | 440 | + out := make([]actionsFilterOption, len(opts)) |
| | 441 | + copy(out, opts) |
| | 442 | + for i := range out { |
| | 443 | + out[i].Selected = out[i].Value == selected |
| | 444 | + } |
| | 445 | + return out |
| | 446 | +} |
| | 447 | + |
| | 448 | +func workflowRunEventLabel(v string) string { |
| | 449 | + switch v { |
| | 450 | + case "pull_request": |
| | 451 | + return "pull request" |
| | 452 | + case "workflow_dispatch": |
| | 453 | + return "workflow dispatch" |
| | 454 | + default: |
| | 455 | + return v |
| | 456 | + } |
| | 457 | +} |
| | 458 | + |
| | 459 | +func actionsPagination(basePath string, filters actionsListFilters, total, pageRows int64) actionsPaginationView { |
| | 460 | + offset := int64((filters.Page - 1) * actionsRunsPageSize) |
| | 461 | + view := actionsPaginationView{ |
| | 462 | + Page: filters.Page, |
| | 463 | + PageSize: actionsRunsPageSize, |
| | 464 | + Total: total, |
| | 465 | + HasPrev: filters.Page > 1, |
| | 466 | + HasNext: offset+pageRows < total, |
| | 467 | + } |
| | 468 | + if total == 0 { |
| | 469 | + view.ResultText = "No workflow runs" |
| | 470 | + return view |
| | 471 | + } |
| | 472 | + view.Start = offset + 1 |
| | 473 | + view.End = offset + pageRows |
| | 474 | + view.ResultText = strconv.FormatInt(view.Start, 10) + "-" + strconv.FormatInt(view.End, 10) + " of " + strconv.FormatInt(total, 10) |
| | 475 | + if view.HasPrev { |
| | 476 | + p := actionsFilterParams(filters) |
| | 477 | + if filters.Page <= 2 { |
| | 478 | + p.Del("page") |
| | 479 | + } else { |
| | 480 | + p.Set("page", strconv.FormatInt(int64(filters.Page-1), 10)) |
| | 481 | + } |
| | 482 | + view.PrevHref = pathWithQuery(basePath, p) |
| | 483 | + } |
| | 484 | + if view.HasNext { |
| | 485 | + p := actionsFilterParams(filters) |
| | 486 | + p.Set("page", strconv.FormatInt(int64(filters.Page+1), 10)) |
| | 487 | + view.NextHref = pathWithQuery(basePath, p) |
| | 488 | + } |
| | 489 | + return view |
| | 490 | +} |
| | 491 | + |
| | 492 | +func actionsFilterParams(filters actionsListFilters) url.Values { |
| | 493 | + v := url.Values{} |
| | 494 | + if filters.Workflow != "" { |
| | 495 | + v.Set("workflow", filters.Workflow) |
| | 496 | + } |
| | 497 | + if filters.Branch != "" { |
| | 498 | + v.Set("branch", filters.Branch) |
| | 499 | + } |
| | 500 | + if filters.Event != "" { |
| | 501 | + v.Set("event", filters.Event) |
| | 502 | + } |
| | 503 | + if filters.Status != "" { |
| | 504 | + v.Set("status", filters.Status) |
| | 505 | + } |
| | 506 | + if filters.Conclusion != "" { |
| | 507 | + v.Set("conclusion", filters.Conclusion) |
| | 508 | + } |
| | 509 | + if filters.Actor != "" { |
| | 510 | + v.Set("actor", filters.Actor) |
| | 511 | + } |
| | 512 | + if filters.Page > 1 { |
| | 513 | + v.Set("page", strconv.FormatInt(int64(filters.Page), 10)) |
| | 514 | + } |
| | 515 | + return v |
| | 516 | +} |
| | 517 | + |
| | 518 | +func cloneValues(v url.Values) url.Values { |
| | 519 | + out := url.Values{} |
| | 520 | + for key, values := range v { |
| | 521 | + for _, value := range values { |
| | 522 | + out.Add(key, value) |
| | 523 | + } |
| | 524 | + } |
| | 525 | + return out |
| | 526 | +} |
| | 527 | + |
| | 528 | +func pathWithQuery(basePath string, q url.Values) string { |
| | 529 | + if encoded := q.Encode(); encoded != "" { |
| | 530 | + return basePath + "?" + encoded |
| | 531 | + } |
| | 532 | + return basePath |
| | 533 | +} |
| | 534 | + |
| 105 | func (h *Handlers) repoActionRun(w http.ResponseWriter, r *http.Request) { | 535 | func (h *Handlers) repoActionRun(w http.ResponseWriter, r *http.Request) { |
| 106 | row, owner, ok := h.loadRepoAndAuthorize(w, r, policy.ActionRepoRead) | 536 | row, owner, ok := h.loadRepoAndAuthorize(w, r, policy.ActionRepoRead) |
| 107 | if !ok { | 537 | if !ok { |
@@ -142,24 +572,6 @@ func (h *Handlers) repoActionRun(w http.ResponseWriter, r *http.Request) { |
| 142 | } | 572 | } |
| 143 | } | 573 | } |
| 144 | | 574 | |
| 145 | -func actionsSuiteViewFromListRow(row checksdb.ListCheckSuitesForRepoRow, runs []checksdb.CheckRun) actionsSuiteView { | | |
| 146 | - return actionsSuiteViewFromParts( | | |
| 147 | - row.ID, | | |
| 148 | - row.HeadSha, | | |
| 149 | - row.AppSlug, | | |
| 150 | - row.Status, | | |
| 151 | - row.Conclusion, | | |
| 152 | - row.CreatedAt.Time, | | |
| 153 | - row.UpdatedAt.Time, | | |
| 154 | - row.PullNumber, | | |
| 155 | - row.PullTitle, | | |
| 156 | - row.PullAuthorUsername, | | |
| 157 | - row.HeadRef, | | |
| 158 | - row.BaseRef, | | |
| 159 | - runs, | | |
| 160 | - ) | | |
| 161 | -} | | |
| 162 | - | | |
| 163 | func actionsSuiteViewFromGetRow(row checksdb.GetCheckSuiteForRepoRow, runs []checksdb.CheckRun) actionsSuiteView { | 575 | func actionsSuiteViewFromGetRow(row checksdb.GetCheckSuiteForRepoRow, runs []checksdb.CheckRun) actionsSuiteView { |
| 164 | return actionsSuiteViewFromParts( | 576 | return actionsSuiteViewFromParts( |
| 165 | row.ID, | 577 | row.ID, |
@@ -197,7 +609,7 @@ func actionsSuiteViewFromParts( |
| 197 | if title == "" { | 609 | if title == "" { |
| 198 | title = appSlug + " checks for " + shortSHA(headSHA) | 610 | title = appSlug + " checks for " + shortSHA(headSHA) |
| 199 | } | 611 | } |
| 200 | - stateText, stateClass, stateIcon := actionState(status, conclusion) | 612 | + stateText, stateClass, stateIcon := checkActionState(status, conclusion) |
| 201 | runViews := make([]actionsRunView, 0, len(runs)) | 613 | runViews := make([]actionsRunView, 0, len(runs)) |
| 202 | annotationCount := 0 | 614 | annotationCount := 0 |
| 203 | for _, run := range runs { | 615 | for _, run := range runs { |
@@ -230,7 +642,7 @@ func actionsSuiteViewFromParts( |
| 230 | } | 642 | } |
| 231 | | 643 | |
| 232 | func actionsRunViewFromRun(run checksdb.CheckRun) actionsRunView { | 644 | func actionsRunViewFromRun(run checksdb.CheckRun) actionsRunView { |
| 233 | - stateText, stateClass, stateIcon := actionState(run.Status, run.Conclusion) | 645 | + stateText, stateClass, stateIcon := checkActionState(run.Status, run.Conclusion) |
| 234 | start := run.CreatedAt.Time | 646 | start := run.CreatedAt.Time |
| 235 | if run.StartedAt.Valid { | 647 | if run.StartedAt.Valid { |
| 236 | start = run.StartedAt.Time | 648 | start = run.StartedAt.Time |
@@ -252,13 +664,13 @@ func actionsRunViewFromRun(run checksdb.CheckRun) actionsRunView { |
| 252 | } | 664 | } |
| 253 | } | 665 | } |
| 254 | | 666 | |
| 255 | -func actionState(status checksdb.CheckStatus, conclusion checksdb.NullCheckConclusion) (string, string, string) { | 667 | +func checkActionState(status checksdb.CheckStatus, conclusion checksdb.NullCheckConclusion) (string, string, string) { |
| 256 | if !conclusion.Valid { | 668 | if !conclusion.Valid { |
| 257 | switch status { | 669 | switch status { |
| 258 | case checksdb.CheckStatusCompleted: | 670 | case checksdb.CheckStatusCompleted: |
| 259 | return "Completed", "neutral", "check-circle" | 671 | return "Completed", "neutral", "check-circle" |
| 260 | case checksdb.CheckStatusInProgress: | 672 | case checksdb.CheckStatusInProgress: |
| 261 | - return "In progress", "pending", "dot-fill" | 673 | + return "In progress", "running", "dot-fill" |
| 262 | case checksdb.CheckStatusQueued, checksdb.CheckStatusPending: | 674 | case checksdb.CheckStatusQueued, checksdb.CheckStatusPending: |
| 263 | return "Queued", "pending", "dot-fill" | 675 | return "Queued", "pending", "dot-fill" |
| 264 | default: | 676 | default: |