-
Notifications
You must be signed in to change notification settings - Fork 0
Fix/popover scrollview jobs display (design-branch-1) #369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
086162e
b05078b
dce66a3
119cae7
93f2695
4556f24
70189fe
87fff9b
114cfa8
4a4a0c5
f998973
e8864f7
b29ac09
668224d
51aadb1
7368e8f
043d484
6c41d6b
0d20819
15021c0
07d6414
9e731bc
75550c9
e00ce1c
888288d
d70d046
43a214c
2da30b8
c8e8118
500bbd3
6a3fe6d
28d973c
0085057
1a143ce
3e1513c
751a4b7
e274658
0e387db
2d98df2
0ee6d84
779ade0
e592344
8a39ad9
8bcadaa
dd3718a
77a7d8e
7162557
4555aa4
8775c11
c80f4fc
64b0fdd
a052859
2668474
c2e3935
1e7c776
dc4a801
8c3619f
ddeb34f
4770a9f
6a6c330
900b8b3
cf49395
bc057a2
d42ba81
e6dc2e0
45c90f1
d413569
1aaede2
4452387
5861038
91842f2
f4317dd
35685f0
9d97e21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .build | ||
| dist |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||
| import Foundation | ||||||||||||
| // swiftlint:disable opening_brace identifier_name missing_docs orphaned_doc_comment | ||||||||||||
| // swiftlint:disable opening_brace identifier_name orphaned_doc_comment | ||||||||||||
|
|
||||||||||||
| // MARK: - GroupStatus | ||||||||||||
|
|
||||||||||||
|
|
@@ -20,10 +20,15 @@ | |||||||||||
| /// Holds only the data needed for display and job fetching — deliberately | ||||||||||||
| /// minimal so the full job list lives on the parent ActionGroup instead. | ||||||||||||
| struct WorkflowRunRef: Identifiable { | ||||||||||||
| /// Unique GitHub run identifier. | ||||||||||||
| let id: Int | ||||||||||||
| let name: String // workflow file name, e.g. "SonarQube", "vitest" | ||||||||||||
| /// Workflow file name, e.g. "SonarQube", "vitest". | ||||||||||||
| let name: String | ||||||||||||
| /// Current run status: `queued`, `in_progress`, or `completed`. | ||||||||||||
| let status: String | ||||||||||||
| /// Final outcome when status is `completed`. | ||||||||||||
| let conclusion: String? | ||||||||||||
| /// URL to the run's page on github.com. | ||||||||||||
| let htmlUrl: String? | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
|
@@ -58,6 +63,7 @@ | |||||||||||
| /// Timestamps derived from job data, not run-level API fields. | ||||||||||||
| /// Mirrors ci-dash.py's `first_job_started_at` / `last_job_completed_at`. | ||||||||||||
| var firstJobStartedAt: Date? | ||||||||||||
| /// Last job completion time across all runs in this group. | ||||||||||||
| var lastJobCompletedAt: Date? | ||||||||||||
|
|
||||||||||||
| /// Fallback creation time from the representative run. | ||||||||||||
|
|
@@ -116,77 +122,66 @@ | |||||||||||
|
|
||||||||||||
| /// Name of the first in-progress job, or first queued, or "—". | ||||||||||||
| var currentJobName: String { | ||||||||||||
| if let j = jobs.first(where: { $0.status == "in_progress" }) { return j.name } | ||||||||||||
| if let j = jobs.first(where: { $0.status == "queued" }) { return j.name } | ||||||||||||
| if let job = jobs.first(where: { $0.status == "in_progress" }) { return job.name } | ||||||||||||
| if let job = jobs.first(where: { $0.status == "queued" }) { return job.name } | ||||||||||||
| return "—" | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// How long ago the group started, as a short human string, e.g. "3m ago", "1h ago". | ||||||||||||
| /// Uses `firstJobStartedAt` when available, falls back to `createdAt`. | ||||||||||||
| /// Returns "—" if neither timestamp is available. | ||||||||||||
| /// Delegates to `RelativeTimeFormatter` for testable, injectable formatting. | ||||||||||||
| var startedAgo: String { | ||||||||||||
| guard let ref = firstJobStartedAt ?? createdAt else { return "—" } | ||||||||||||
| return RelativeTimeFormatter.string(from: ref) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// Elapsed time derived from min(job.startedAt) → max(job.completedAt). | ||||||||||||
| var elapsed: String { | ||||||||||||
| if let start = firstJobStartedAt { | ||||||||||||
| let end = lastJobCompletedAt ?? Date() | ||||||||||||
| let sec = Int(end.timeIntervalSince(start)) | ||||||||||||
| guard sec >= 0 else { return "00:00" } | ||||||||||||
| let m = sec / 60; let s = sec % 60 | ||||||||||||
| return String(format: "%02d:%02d", m, s) | ||||||||||||
| let minutes = sec / 60; let seconds = sec % 60 | ||||||||||||
| return String(format: "%02d:%02d", minutes, seconds) | ||||||||||||
| } | ||||||||||||
| guard let start = createdAt else { return "00:00" } | ||||||||||||
| let sec = Int(Date().timeIntervalSince(start)) | ||||||||||||
| guard sec >= 0 else { return "00:00" } | ||||||||||||
| let m = sec / 60; let s = sec % 60 | ||||||||||||
| return String(format: "%02d:%02d", m, s) | ||||||||||||
| let minutes = sec / 60; let seconds = sec % 60 | ||||||||||||
| return String(format: "%02d:%02d", minutes, seconds) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // MARK: - Codable helpers (private to this file) | ||||||||||||
|
|
||||||||||||
| private struct ActionRunsResponse: Codable { | ||||||||||||
| let workflowRuns: [RunPayload] | ||||||||||||
| enum CodingKeys: String, CodingKey { case workflowRuns = "workflow_runs" } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| private struct RunPayload: Codable { | ||||||||||||
| let id: Int | ||||||||||||
| let name: String | ||||||||||||
| let status: String | ||||||||||||
| let conclusion: String? | ||||||||||||
| let headBranch: String? | ||||||||||||
| let headSha: String | ||||||||||||
| let displayTitle: String? | ||||||||||||
| let createdAt: String? | ||||||||||||
| let updatedAt: String? | ||||||||||||
| let htmlUrl: String? | ||||||||||||
| let headCommit: HeadCommit? | ||||||||||||
| let pullRequests: [PRRef]? | ||||||||||||
|
|
||||||||||||
| enum CodingKeys: String, CodingKey { | ||||||||||||
| case id, name, status, conclusion | ||||||||||||
| case headBranch = "head_branch" | ||||||||||||
| case headSha = "head_sha" | ||||||||||||
| case displayTitle = "display_title" | ||||||||||||
| case createdAt = "created_at" | ||||||||||||
| case updatedAt = "updated_at" | ||||||||||||
| case htmlUrl = "html_url" | ||||||||||||
| case headCommit = "head_commit" | ||||||||||||
| case pullRequests = "pull_requests" | ||||||||||||
| // MARK: - Progress fraction (model layer — keeps view bodies clean) | ||||||||||||
|
|
||||||||||||
| /// Completion fraction 0.0–1.0 for the pie-progress dot, or `nil` (indeterminate) | ||||||||||||
| /// when status is queued or no job data is available. | ||||||||||||
| /// | ||||||||||||
| /// - `nil` → indeterminate (queued / no jobs loaded yet) | ||||||||||||
| /// - `1.0` → completed | ||||||||||||
| /// - `0..<1` → partial (jobsDone / jobsTotal) | ||||||||||||
| var progressFraction: Double? { | ||||||||||||
| switch groupStatus { | ||||||||||||
| case .queued: return nil | ||||||||||||
| case .completed: return 1.0 | ||||||||||||
| case .inProgress: | ||||||||||||
| guard jobsTotal > 0 else { return nil } | ||||||||||||
| return Double(jobsDone) / Double(jobsTotal) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| private struct HeadCommit: Codable { let message: String } | ||||||||||||
| private struct PRRef: Codable { let number: Int } | ||||||||||||
|
|
||||||||||||
| // MARK: - PR label | ||||||||||||
| // MARK: - Equatable | ||||||||||||
|
|
||||||||||||
| /// Derives the short identifier for an action group row. | ||||||||||||
| /// Priority: PR number → branch-embedded number → sha[:7]. | ||||||||||||
| private func prLabel(from run: RunPayload) -> String { | ||||||||||||
| if let pr = run.pullRequests?.first { return "#\(pr.number)" } | ||||||||||||
| if let branch = run.headBranch, | ||||||||||||
| let range = branch.range(of: #"/(\d+)/"#, options: .regularExpression) { | ||||||||||||
| let digits = branch[range].filter { $0.isNumber } | ||||||||||||
| return "#\(digits)" | ||||||||||||
| extension ActionGroup: Equatable { | ||||||||||||
| /// Returns `true` when two groups have the same ID, dimmed state, job list, and run IDs. | ||||||||||||
| // swiftlint:disable:next missing_docs | ||||||||||||
| static func == (lhs: ActionGroup, rhs: ActionGroup) -> Bool { | ||||||||||||
|
Check failure on line 179 in Sources/RunnerBar/ActionGroup.swift
|
||||||||||||
|
Comment on lines
+178
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove superfluous SwiftLint disable directive. SwiftLint reports that 🔧 Proposed fix extension ActionGroup: Equatable {
/// Returns `true` when two groups have the same ID, dimmed state, job list, and run IDs.
- // swiftlint:disable:next missing_docs
static func == (lhs: ActionGroup, rhs: ActionGroup) -> Bool {📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: SwiftLint[failure] 179-179: 🤖 Prompt for AI Agents |
||||||||||||
| lhs.id == rhs.id | ||||||||||||
| && lhs.isDimmed == rhs.isDimmed | ||||||||||||
| && lhs.jobs == rhs.jobs | ||||||||||||
| && lhs.runs.map({ $0.id }) == rhs.runs.map({ $0.id }) | ||||||||||||
| } | ||||||||||||
| return String(run.headSha.prefix(7)) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // MARK: - Fetch + Group | ||||||||||||
|
|
@@ -195,7 +190,7 @@ | |||||||||||
| /// enriches each group with its flattened job list, and returns groups sorted: | ||||||||||||
| /// in_progress first, then queued, then done — newest first. | ||||||||||||
| // swiftlint:disable:next function_body_length cyclomatic_complexity | ||||||||||||
| func fetchActionGroups(for scope: String, cache: [String: ActionGroup] = [:]) -> [ActionGroup] { | ||||||||||||
|
Check failure on line 193 in Sources/RunnerBar/ActionGroup.swift
|
||||||||||||
| guard scope.contains("/") else { | ||||||||||||
| log("fetchActionGroups › skipping org scope \(scope)") | ||||||||||||
| return [] | ||||||||||||
|
|
@@ -299,27 +294,27 @@ | |||||||||||
| // MARK: - Private helpers | ||||||||||||
|
|
||||||||||||
| /// Constructs an `ActiveJob` from a decoded `JobPayload`. | ||||||||||||
| func makeActiveJob(from j: JobPayload, iso: ISO8601DateFormatter, | ||||||||||||
| func makeActiveJob(from payload: JobPayload, iso: ISO8601DateFormatter, | ||||||||||||
| isDimmed: Bool = false) -> ActiveJob { | ||||||||||||
| let steps: [JobStep] = (j.steps ?? []).enumerated().map { idx, s in | ||||||||||||
| let steps: [JobStep] = (payload.steps ?? []).enumerated().map { idx, step in | ||||||||||||
| JobStep( | ||||||||||||
| id: idx + 1, | ||||||||||||
| name: s.name, | ||||||||||||
| status: s.status, | ||||||||||||
| conclusion: s.conclusion, | ||||||||||||
| startedAt: s.startedAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| completedAt: s.completedAt.flatMap { iso.date(from: $0) } | ||||||||||||
| name: step.name, | ||||||||||||
| status: step.status, | ||||||||||||
| conclusion: step.conclusion, | ||||||||||||
| startedAt: step.startedAt, | ||||||||||||
| completedAt: step.completedAt | ||||||||||||
| ) | ||||||||||||
| } | ||||||||||||
| return ActiveJob( | ||||||||||||
| id: j.id, | ||||||||||||
| name: j.name, | ||||||||||||
| status: j.status, | ||||||||||||
| conclusion: j.conclusion, | ||||||||||||
| startedAt: j.startedAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| createdAt: j.createdAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| completedAt: j.completedAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| htmlUrl: j.htmlUrl, | ||||||||||||
| id: payload.id, | ||||||||||||
| name: payload.name, | ||||||||||||
| status: payload.status, | ||||||||||||
| conclusion: payload.conclusion, | ||||||||||||
| startedAt: payload.startedAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| createdAt: payload.createdAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| completedAt: payload.completedAt.flatMap { iso.date(from: $0) }, | ||||||||||||
| htmlUrl: payload.htmlUrl, | ||||||||||||
| isDimmed: isDimmed, | ||||||||||||
| steps: steps | ||||||||||||
| ) | ||||||||||||
|
|
@@ -381,4 +376,4 @@ | |||||||||||
| case .completed: return 2 | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| // swiftlint:enable opening_brace identifier_name missing_docs orphaned_doc_comment | ||||||||||||
| // swiftlint:enable opening_brace identifier_name orphaned_doc_comment | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The progress calculation uses
jobsDone, butjobsDoneonly countssuccessandskipped, not other concluded states likefailure/cancelled. This makes in-progress groups appear less complete than they really are whenever some jobs have already finished unsuccessfully. Compute the fraction from all concluded jobs (conclusion != nil) so the pie reflects true completion progress. [logic error]Severity Level: Major⚠️
Steps of Reproduction ✅
Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖