neiam
/gitgud-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Follow the forge's newer CI vocabulary
010513e · Gabriel Morell · 2026-10-19 16:51
Files changed
modified
README.md
+22
−4
@@ -7,7 +7,24 @@ A Plasma 6 panel widget that shows [GitGud](https://gg.neiam.co) CI runs as a ro
| 7 | 7 | ``` |
| 8 | 8 | |
| 9 | 9 | - **Logo**: opens a popup with every recent run across everything you watch. |
| 10 | −- **Circles**: one per watched repository, or one per organization/user if you fold it into a single circle. The colour shows the latest run: green is success, red is failure or cancelled, a pulsing orange circle is running, and a hollow orange ring is queued. Hover a circle for the workflow, run number, ref, SHA and age. Click it to open just that circle's runs. | |
| 10 | +- **Circles**: one per watched repository, or one per organization/user if you fold it into a single circle. The colour shows the latest run (see [Statuses](#statuses)). Hover a circle for the workflow, run number, ref, SHA and age. Click it to open just that circle's runs. | |
| 11 | + | |
| 12 | +## Statuses | |
| 13 | + | |
| 14 | +Statuses read as they do in the Android app and the `gg` CLI. | |
| 15 | + | |
| 16 | +| Mark | Status | Meaning | | |
| 17 | +|---|---|---| | |
| 18 | +| green dot | success | | | |
| 19 | +| red dot | failure, timed out, cancelled | | | |
| 20 | +| pulsing orange dot | running | | | |
| 21 | +| hollow orange ring | queued, blocked, waiting | not started yet: waiting for a runner, for its `needs`, or held by its concurrency group | | |
| 22 | +| orange ring with a dot | needs approval | a fork pull request's run, waiting for someone with write access to approve it on the pull request | | |
| 23 | +| grey dot | skipped, superseded | superseded means a newer run or job in its concurrency group replaced it. It isn't a failure. | | |
| 24 | + | |
| 25 | +Only success, failure, cancelled, skipped and timed out are endings. Anything else, including a status the widget doesn't know yet, counts as still in progress and keeps being refreshed. A run waiting for approval is refreshed at the normal pace, not the fast one, because it's waiting on a person. | |
| 26 | + | |
| 27 | +Triggers read as words: *scheduled*, *manual* (`workflow_dispatch`), *pull request (target)*, *branch/tag created/deleted*, and *after a workflow* (`workflow_run`). A run from a fork says *from a fork*. | |
| 11 | 28 | |
| 12 | 29 | ## Run accordion |
| 13 | 30 |
@@ -24,7 +41,8 @@ Runs expand the same way as in the Android app:
| 24 | 41 | ``` |
| 25 | 42 | |
| 26 | 43 | - Click a **run** to see its jobs, artifacts, and the **Re-run all**, **Cancel** and **Open** buttons. |
| 27 | −- Click a **job** to see its steps. The ⟳ button re-runs only that job. | |
| 44 | +- Click a **job** to see its steps. The ⟳ button re-runs only that job. Under the job's status you'll see why it is waiting, was skipped or was cancelled, and *attempt N* once it has been re-run. A dynamic matrix that hasn't expanded yet says *Matrix expands when … finish*. Its jobs replace it once they're created. | |
| 45 | +- A fork's run that is **waiting for approval** says so and links to its pull request. You approve it there. | |
| 28 | 46 | - Click a **step** to see its log slice. Failed steps open on their own. |
| 29 | 47 | |
| 30 | 48 | Open runs keep refreshing while they're in flight. Re-run and cancel are only shown when the server says you can manage the repository and the token has `ci:write`. |
@@ -68,8 +86,8 @@ kpackagetool6 --type Plasma/Applet --install .
| 68 | 86 | kpackagetool6 --type Plasma/Applet --upgrade . |
| 69 | 87 | |
| 70 | 88 | # Or build a release archive |
| 71 | −./package.sh 0.1.0 | |
| 72 | −kpackagetool6 --type Plasma/Applet --install gitgud-plasmoid-0.1.0.tar.gz | |
| 89 | +./package.sh 0.2.0 | |
| 90 | +kpackagetool6 --type Plasma/Applet --install gitgud-plasmoid-0.2.0.tar.gz | |
| 73 | 91 | ``` |
| 74 | 92 | |
| 75 | 93 | Test without installing: |
modified
contents/ui/GitGudAPI.qml
+4
−0
@@ -196,6 +196,10 @@ QtObject {
| 196 | 196 | request("POST", repoPath(fullName) + "/runs/" + number + "/cancel", callback) |
| 197 | 197 | } |
| 198 | 198 | |
| 199 | + function pullUrl(fullName, number) { | |
| 200 | + return normalizedBase() + "/r/" + fullName + "/pulls/" + number | |
| 201 | + } | |
| 202 | + | |
| 199 | 203 | function webUrl(fullName, runNumber) { |
| 200 | 204 | var url = normalizedBase() + "/r/" + fullName |
| 201 | 205 | if (runNumber !== undefined) url += "/actions/" + runNumber |
modified
contents/ui/JobItem.qml
+21
−4
@@ -21,7 +21,8 @@ ColumnLayout {
| 21 | 21 | |
| 22 | 22 | readonly property bool expanded: store.expandedJobs[job.id] === true |
| 23 | 23 | readonly property var logs: store.jobLogs[job.id] || null |
| 24 | − readonly property string status: Status.effective(job.status, job.conclusion) | |
| 24 | + readonly property string status: Status.jobStatus(job) | |
| 25 | + readonly property string reason: Status.jobReason(job) | |
| 25 | 26 | |
| 26 | 27 | spacing: 0 |
| 27 | 28 |
@@ -67,15 +68,30 @@ ColumnLayout {
| 67 | 68 | elide: Text.ElideRight |
| 68 | 69 | font: Kirigami.Theme.smallFont |
| 69 | 70 | opacity: 0.7 |
| 70 | − text: (jobItem.store.now, [jobItem.status, | |
| 71 | + text: (jobItem.store.now, [Status.label(jobItem.status), | |
| 72 | + jobItem.job.attempt > 1 ? "attempt " + jobItem.job.attempt : "", | |
| 71 | 73 | Status.duration(jobItem.job.started_at, jobItem.job.completed_at), |
| 72 | 74 | (jobItem.job.needs && jobItem.job.needs.length > 0) ? "needs " + jobItem.job.needs.join(", ") : ""] |
| 73 | 75 | .filter(function(s) { return s !== "" }).join(" · ")) |
| 74 | 76 | } |
| 77 | + | |
| 78 | + // Why it's waiting, skipped or cancelled — or, for a | |
| 79 | + // matrix placeholder, when it expands. | |
| 80 | + PlasmaComponents3.Label { | |
| 81 | + visible: jobItem.reason !== "" | |
| 82 | + Layout.fillWidth: true | |
| 83 | + wrapMode: Text.WordWrap | |
| 84 | + maximumLineCount: 3 | |
| 85 | + elide: Text.ElideRight | |
| 86 | + font: Kirigami.Theme.smallFont | |
| 87 | + opacity: 0.7 | |
| 88 | + text: jobItem.reason | |
| 89 | + } | |
| 75 | 90 | } |
| 76 | 91 | |
| 77 | 92 | PlasmaComponents3.ToolButton { |
| 78 | − visible: jobItem.canManage && !!jobItem.job.job_id | |
| 93 | + // A placeholder isn't a job yet: its legs replace it. | |
| 94 | + visible: jobItem.canManage && !!jobItem.job.job_id && !jobItem.job.placeholder | |
| 79 | 95 | enabled: !jobItem.store.busy |
| 80 | 96 | icon.name: "view-refresh" |
| 81 | 97 | onClicked: jobItem.store.rerunJob(jobItem.fullName, jobItem.runNumber, jobItem.job.job_id) |
@@ -93,7 +109,8 @@ ColumnLayout {
| 93 | 109 | |
| 94 | 110 | PlasmaComponents3.Label { |
| 95 | 111 | visible: jobItem.job.steps.length === 0 |
| 96 | − text: i18n("No steps yet") | |
| 112 | + text: jobItem.job.placeholder ? i18n("Matrix not expanded yet — its jobs appear here once it does") | |
| 113 | + : i18n("No steps yet") | |
| 97 | 114 | opacity: 0.7 |
| 98 | 115 | font: Kirigami.Theme.smallFont |
| 99 | 116 | } |
modified
contents/ui/RunItem.qml
+34
−2
@@ -22,8 +22,10 @@ ColumnLayout {
| 22 | 22 | readonly property string runKey: fullName + "#" + run.number |
| 23 | 23 | readonly property bool expanded: store.expandedRuns[runKey] === true |
| 24 | 24 | readonly property var detail: store.runDetails[runKey] || null |
| 25 | − readonly property string status: Status.effective(run.status, run.conclusion) | |
| 25 | + readonly property string status: Status.runStatus(run) | |
| 26 | 26 | readonly property bool active: Status.isActive(run.status) |
| 27 | + // The detail's copy is fresher than the list row's once it's loaded. | |
| 28 | + readonly property var current: detail ? detail.run : run | |
| 27 | 29 | |
| 28 | 30 | spacing: 0 |
| 29 | 31 |
@@ -75,7 +77,7 @@ ColumnLayout {
| 75 | 77 | Layout.alignment: Qt.AlignRight |
| 76 | 78 | font: Kirigami.Theme.smallFont |
| 77 | 79 | color: Status.color(runItem.status, Kirigami.Theme) |
| 78 | − text: runItem.status | |
| 80 | + text: Status.label(runItem.status) | |
| 79 | 81 | } |
| 80 | 82 | |
| 81 | 83 | PlasmaComponents3.Label { |
@@ -105,6 +107,36 @@ ColumnLayout {
| 105 | 107 | Layout.preferredHeight: Kirigami.Units.iconSizes.medium |
| 106 | 108 | } |
| 107 | 109 | |
| 110 | + // A fork's run waits for someone with write access to approve | |
| 111 | + // it, and that happens on the pull request — say so, link there. | |
| 112 | + ColumnLayout { | |
| 113 | + visible: !!runItem.current.awaiting_approval | |
| 114 | + Layout.fillWidth: true | |
| 115 | + spacing: 0 | |
| 116 | + | |
| 117 | + PlasmaComponents3.Label { | |
| 118 | + Layout.fillWidth: true | |
| 119 | + color: Kirigami.Theme.neutralTextColor | |
| 120 | + text: i18n("Waiting for approval") | |
| 121 | + } | |
| 122 | + | |
| 123 | + PlasmaComponents3.Label { | |
| 124 | + Layout.fillWidth: true | |
| 125 | + wrapMode: Text.WordWrap | |
| 126 | + font: Kirigami.Theme.smallFont | |
| 127 | + opacity: 0.7 | |
| 128 | + text: Status.approvalNotice(runItem.current) | |
| 129 | + } | |
| 130 | + | |
| 131 | + PlasmaComponents3.ToolButton { | |
| 132 | + visible: !!runItem.current.pull_request_number | |
| 133 | + icon.name: "internet-web-browser" | |
| 134 | + text: i18n("Open pull request #%1", runItem.current.pull_request_number) | |
| 135 | + display: QQC2.AbstractButton.TextBesideIcon | |
| 136 | + onClicked: Qt.openUrlExternally(runItem.store.api.pullUrl(runItem.fullName, runItem.current.pull_request_number)) | |
| 137 | + } | |
| 138 | + } | |
| 139 | + | |
| 108 | 140 | RowLayout { |
| 109 | 141 | visible: !!runItem.detail |
| 110 | 142 | Layout.fillWidth: true |
modified
contents/ui/Status.js
+100
−9
@@ -1,10 +1,18 @@
| 1 | 1 | .pragma library |
| 2 | 2 | |
| 3 | 3 | // CI status vocabulary, shared by every view. Mirrors the Android |
| 4 | −// client's StatusChip.kt so the widget and the phone agree on what a | |
| 5 | −// colour means. | |
| 6 | − | |
| 7 | −var ACTIVE = ["queued", "running", "pending", "blocked"] | |
| 4 | +// client's StatusChip.kt (and the `gg` CLI's output.rs) so the widget, | |
| 5 | +// the phone and the terminal agree on what a colour means. | |
| 6 | +// | |
| 7 | +// Two names here aren't server statuses: `awaiting_approval` on a run | |
| 8 | +// (runStatus — the server says `queued`) and `superseded`, a job | |
| 9 | +// cancelled because a newer one in its concurrency group replaced it | |
| 10 | +// (jobStatus). Superseded is grey, not red: the newer run decides. | |
| 11 | + | |
| 12 | +// The only endings. Anything else — waiting, awaiting_approval, or a | |
| 13 | +// status the server learns later — is still in progress, so polling | |
| 14 | +// keeps going rather than calling it done. | |
| 15 | +var FINISHED = ["success", "failure", "cancelled", "skipped", "timed_out", "superseded"] | |
| 8 | 16 | |
| 9 | 17 | // `conclusion` when the thing has finished, else its raw `status`. |
| 10 | 18 | function effective(status, conclusion) { |
@@ -12,8 +20,37 @@ function effective(status, conclusion) {
| 12 | 20 | return status || "unknown" |
| 13 | 21 | } |
| 14 | 22 | |
| 23 | +// What a run's circle says. A fork's run waiting for approval is | |
| 24 | +// `queued` on the server, but "queued" would promise a runner is | |
| 25 | +// coming — nothing happens until someone approves it. | |
| 26 | +function runStatus(run) { | |
| 27 | + if (!run) return "unknown" | |
| 28 | + if (run.awaiting_approval && (run.status === "queued" || run.status === "running")) return "awaiting_approval" | |
| 29 | + return effective(run.status, run.conclusion) | |
| 30 | +} | |
| 31 | + | |
| 32 | +// `superseded` for a job a newer one replaced, else its effective status. | |
| 33 | +function jobStatus(job) { | |
| 34 | + if (!job) return "unknown" | |
| 35 | + if (job.superseded) return "superseded" | |
| 36 | + return effective(job.status, job.conclusion) | |
| 37 | +} | |
| 38 | + | |
| 39 | +function isFinished(status) { | |
| 40 | + return FINISHED.indexOf(status) !== -1 || status === "completed" || status === "finished" | |
| 41 | +} | |
| 42 | + | |
| 43 | +// Not finished. "unknown" is this widget's own word for "nothing known | |
| 44 | +// yet" (a repo that never ran CI), not a server status, so it's idle. | |
| 15 | 45 | function isActive(status) { |
| 16 | − return ACTIVE.indexOf(status) !== -1 | |
| 46 | + return !!status && status !== "unknown" && !isFinished(status) | |
| 47 | +} | |
| 48 | + | |
| 49 | +// Worth the fast poll: in flight, but not parked on a person. A run | |
| 50 | +// waiting for approval is still watched on every poll, just at the idle | |
| 51 | +// pace; once approved it's `queued` again and the fast poll picks it up. | |
| 52 | +function pollsFast(status) { | |
| 53 | + return isActive(status) && status !== "awaiting_approval" | |
| 17 | 54 | } |
| 18 | 55 | |
| 19 | 56 | // success | failure | running | queued | dim |
@@ -30,9 +67,16 @@ function category(status) {
| 30 | 67 | case "queued": |
| 31 | 68 | case "pending": |
| 32 | 69 | case "blocked": |
| 70 | + case "waiting": | |
| 71 | + case "awaiting_approval": | |
| 33 | 72 | return "queued" |
| 34 | − default: | |
| 73 | + case "skipped": | |
| 74 | + case "superseded": | |
| 35 | 75 | return "dim" |
| 76 | + default: | |
| 77 | + // A status this widget doesn't know yet but that hasn't ended | |
| 78 | + // reads as pending, not as idle. | |
| 79 | + return isActive(status) ? "queued" : "dim" | |
| 36 | 80 | } |
| 37 | 81 | } |
| 38 | 82 |
@@ -51,16 +95,45 @@ function icon(status) {
| 51 | 95 | case "success": return "emblem-success" |
| 52 | 96 | case "failure": |
| 53 | 97 | case "timed_out": return "emblem-error" |
| 54 | − case "cancelled": return "dialog-cancel" | |
| 98 | + case "cancelled": | |
| 99 | + case "superseded": return "dialog-cancel" | |
| 55 | 100 | case "running": return "media-playback-start" |
| 56 | 101 | case "queued": |
| 57 | 102 | case "pending": |
| 58 | − case "blocked": return "chronometer" | |
| 103 | + case "blocked": | |
| 104 | + case "waiting": return "chronometer" | |
| 105 | + case "awaiting_approval": return "flag" | |
| 59 | 106 | case "skipped": return "go-next-skip" |
| 60 | 107 | default: return "question" |
| 61 | 108 | } |
| 62 | 109 | } |
| 63 | 110 | |
| 111 | +// A status as words: `awaiting_approval` reads "needs approval", | |
| 112 | +// `timed_out` "timed out". | |
| 113 | +function label(status) { | |
| 114 | + if (!status) return "unknown" | |
| 115 | + if (status === "awaiting_approval") return "needs approval" | |
| 116 | + return status.replace(/_/g, " ") | |
| 117 | +} | |
| 118 | + | |
| 119 | +// What started a run, as a person would say it. Unknown events fall | |
| 120 | +// through as their raw name with underscores spaced. | |
| 121 | +function triggerLabel(trigger) { | |
| 122 | + switch (trigger) { | |
| 123 | + case undefined: | |
| 124 | + case null: | |
| 125 | + case "": return "" | |
| 126 | + case "pull_request": return "pull request" | |
| 127 | + case "pull_request_target": return "pull request (target)" | |
| 128 | + case "workflow_dispatch": return "manual" | |
| 129 | + case "schedule": return "scheduled" | |
| 130 | + case "create": return "branch/tag created" | |
| 131 | + case "delete": return "branch/tag deleted" | |
| 132 | + case "workflow_run": return "after a workflow" | |
| 133 | + default: return String(trigger).replace(/_/g, " ") | |
| 134 | + } | |
| 135 | +} | |
| 136 | + | |
| 64 | 137 | // Worst-first ordering for folding several repos into one circle: a |
| 65 | 138 | // run in flight is the most interesting thing, then a broken build. |
| 66 | 139 | var PRIORITY = { running: 5, queued: 4, failure: 3, success: 2, dim: 1 } |
@@ -124,11 +197,29 @@ function runMeta(run) {
| 124 | 197 | var parts = [] |
| 125 | 198 | if (run.head_ref) parts.push(shortRef(run.head_ref)) |
| 126 | 199 | if (run.head_sha) parts.push(shortSha(run.head_sha)) |
| 127 | − if (run.trigger) parts.push(run.trigger) | |
| 200 | + if (run.trigger) parts.push(triggerLabel(run.trigger)) | |
| 201 | + if (run.fork) parts.push("from a fork") | |
| 128 | 202 | if (run.triggered_by && run.triggered_by.handle) parts.push("by " + run.triggered_by.handle) |
| 129 | 203 | return parts.join(" · ") |
| 130 | 204 | } |
| 131 | 205 | |
| 206 | +// What to say under a job: the server's reason (why it waits, was | |
| 207 | +// skipped or cancelled, or why its matrix couldn't expand), or for a | |
| 208 | +// matrix placeholder with nothing to explain yet, what it is. | |
| 209 | +function jobReason(job) { | |
| 210 | + if (job.status_reason && String(job.status_reason).trim() !== "") return job.status_reason | |
| 211 | + if (job.placeholder && job.needs && job.needs.length > 0 && jobStatus(job) === "blocked") { | |
| 212 | + return "Matrix expands when " + job.needs.join(", ") + " finish" | |
| 213 | + } | |
| 214 | + return "" | |
| 215 | +} | |
| 216 | + | |
| 217 | +// "Waiting for approval" notice for a fork's run nobody has approved. | |
| 218 | +function approvalNotice(run) { | |
| 219 | + return "This run comes from a fork. Its jobs start once someone with write access approves it" | |
| 220 | + + (run.pull_request_number ? " on pull request #" + run.pull_request_number + "." : ".") | |
| 221 | +} | |
| 222 | + | |
| 132 | 223 | function jobLabel(job) { |
| 133 | 224 | var label = job.name || job.job_id || ("job " + job.id) |
| 134 | 225 | if (job.matrix && typeof job.matrix === "object") { |
modified
contents/ui/StatusCircle.qml
+13
−2
@@ -3,8 +3,10 @@ import org.kde.kirigami as Kirigami
| 3 | 3 | |
| 4 | 4 | import "Status.js" as Status |
| 5 | 5 | |
| 6 | −// A single status dot. Running pulses; queued is a hollow ring, so the | |
| 7 | −// two in-flight states read differently at panel size. | |
| 6 | +// A single status dot. Running pulses; queued (and waiting, blocked) is | |
| 7 | +// a hollow ring, so the two in-flight states read differently at panel | |
| 8 | +// size. Needs-approval is a ring with a dot in it: pending colour, but | |
| 9 | +// its own mark, since it waits on a person rather than a runner. | |
| 8 | 10 | Item { |
| 9 | 11 | id: circle |
| 10 | 12 |
@@ -53,4 +55,13 @@ Item {
| 53 | 55 | Behavior on width { NumberAnimation { duration: Kirigami.Units.shortDuration } } |
| 54 | 56 | Behavior on color { ColorAnimation { duration: Kirigami.Units.longDuration } } |
| 55 | 57 | } |
| 58 | + | |
| 59 | + Rectangle { | |
| 60 | + visible: circle.status === "awaiting_approval" | |
| 61 | + anchors.centerIn: parent | |
| 62 | + width: parent.width * 0.25 | |
| 63 | + height: width | |
| 64 | + radius: width / 2 | |
| 65 | + color: circle.statusColor | |
| 66 | + } | |
| 56 | 67 | } |
modified
contents/ui/configBehavior.qml
+1
−1
@@ -34,7 +34,7 @@ KCM.SimpleKCM {
| 34 | 34 | wrapMode: Text.WordWrap |
| 35 | 35 | font: Kirigami.Theme.smallFont |
| 36 | 36 | color: Kirigami.Theme.disabledTextColor |
| 37 | − text: i18n("While anything is queued or running the widget polls faster, and only refetches repositories whose status moved.") | |
| 37 | + text: i18n("While anything is queued, waiting or running the widget polls faster, and only refetches repositories whose status moved or hasn't finished. A fork's run waiting for approval is checked at the normal pace.") | |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | 40 | QQC2.SpinBox { |
modified
contents/ui/main.qml
+13
−7
@@ -166,7 +166,7 @@ PlasmoidItem {
| 166 | 166 | |
| 167 | 167 | function repoStatus(fullName) { |
| 168 | 168 | var run = latestRun(fullName) |
| 169 | − if (run) return Status.effective(run.status, run.conclusion) | |
| 169 | + if (run) return Status.runStatus(run) | |
| 170 | 170 | var row = repoIndex[fullName] |
| 171 | 171 | return row && row.ci_status ? row.ci_status : "unknown" |
| 172 | 172 | } |
@@ -184,11 +184,11 @@ PlasmoidItem {
| 184 | 184 | lines.push(prefix + i18n("no runs")) |
| 185 | 185 | continue |
| 186 | 186 | } |
| 187 | − var status = Status.effective(run.status, run.conclusion) | |
| 187 | + var status = Status.runStatus(run) | |
| 188 | 188 | var meta = [Status.shortRef(run.head_ref), Status.shortSha(run.head_sha), Status.relativeTime(run.inserted_at)] |
| 189 | 189 | .filter(function(s) { return s !== "" }).join(" · ") |
| 190 | 190 | lines.push(prefix + Status.runTitle(run) + " #" + run.number |
| 191 | − + " — <font color=\"" + Status.color(status, Kirigami.Theme) + "\">" + status + "</font>" | |
| 191 | + + " — <font color=\"" + Status.color(status, Kirigami.Theme) + "\">" + Status.label(status) + "</font>" | |
| 192 | 192 | + (meta ? "<br/><small>" + meta + "</small>" : "")) |
| 193 | 193 | } |
| 194 | 194 | if (repos.length === 0) lines.push(i18n("No repositories found in this namespace")) |
@@ -229,7 +229,9 @@ PlasmoidItem {
| 229 | 229 | for (var j = 0; j < c.repos.length; j++) { |
| 230 | 230 | var s = repoStatus(c.repos[j]) |
| 231 | 231 | statuses.push(s) |
| 232 | − if (Status.isActive(s)) active = true | |
| 232 | + // A run held for approval is still watched every poll, | |
| 233 | + // but only at the idle pace: it waits on a person. | |
| 234 | + if (Status.pollsFast(s)) active = true | |
| 233 | 235 | } |
| 234 | 236 | var status = Status.aggregate(statuses) |
| 235 | 237 | if (hideIdle && status === "unknown") continue |
@@ -362,7 +364,8 @@ PlasmoidItem {
| 362 | 364 | if (runs[i].number === number) { summary = runs[i]; break } |
| 363 | 365 | } |
| 364 | 366 | if (!detail || Status.isActive(detail.run.status) |
| 365 | − || (summary && (summary.status !== detail.run.status || summary.conclusion !== detail.run.conclusion))) { | |
| 367 | + || (summary && (summary.status !== detail.run.status || summary.conclusion !== detail.run.conclusion | |
| 368 | + || !!summary.awaiting_approval !== !!detail.run.awaiting_approval))) { | |
| 366 | 369 | loadRunDetail(fullName, number) |
| 367 | 370 | } |
| 368 | 371 | } |
@@ -435,9 +438,12 @@ PlasmoidItem {
| 435 | 438 | api.getRun(fullName, number, function(ok, data) { |
| 436 | 439 | if (!ok) { lastError = data; return } |
| 437 | 440 | runDetails = setKey(runDetails, key, data) |
| 438 | − // Keep open jobs' logs following the run. | |
| 441 | + // Keep open jobs' logs following the run. A matrix | |
| 442 | + // placeholder has no logs: its row is replaced by its legs | |
| 443 | + // (new ids) once it expands, and this re-read shows them. | |
| 439 | 444 | var jobs = data.jobs || [] |
| 440 | 445 | for (var i = 0; i < jobs.length; i++) { |
| 446 | + if (jobs[i].placeholder) continue | |
| 441 | 447 | if (expandedJobs[jobs[i].id] && (Status.isActive(jobs[i].status) || !jobLogs[jobs[i].id] |
| 442 | 448 | || jobLogs[jobs[i].id].job.status !== jobs[i].status)) { |
| 443 | 449 | loadJobLogs(fullName, jobs[i].id) |
@@ -465,7 +471,7 @@ PlasmoidItem {
| 465 | 471 | function toggleJob(fullName, job) { |
| 466 | 472 | var open = !expandedJobs[job.id] |
| 467 | 473 | expandedJobs = setKey(expandedJobs, job.id, open ? true : undefined) |
| 468 | − if (open) loadJobLogs(fullName, job.id) | |
| 474 | + if (open && !job.placeholder) loadJobLogs(fullName, job.id) | |
| 469 | 475 | } |
| 470 | 476 | |
| 471 | 477 | function stepOpen(stepKey, status) { |
modified
metadata.json
+1
−1
@@ -11,7 +11,7 @@
| 11 | 11 | "Icon": "run-build", |
| 12 | 12 | "Id": "org.neiam.kde.gitgud", |
| 13 | 13 | "Name": "GitGud CI", |
| 14 | − "Version": "0.1.0", | |
| 14 | + "Version": "0.2.0", | |
| 15 | 15 | "Website": "http://neiam.org" |
| 16 | 16 | }, |
| 17 | 17 | "KPackageStructure": "Plasma/Applet", |
Parents: db86591