feat(ui): show cancel button until all jobs are finished (#9261)

Change that the Cancel button is shown until all jobs are finished and do not hide it, when the first job failed.
Additionally the wrapping of the header was changed.

| Before | After |
| :--: | :----: |
| ![grafik](/attachments/26ee51ab-9a46-4c91-a866-e01cb0e97b28) | ![grafik](/attachments/eda3769b-e555-4964-9644-06f772046247) |

Fixes #8922

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9261
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Beowulf <beowulf@beocode.eu>
Co-committed-by: Beowulf <beowulf@beocode.eu>
This commit is contained in:
Beowulf 2025-12-21 17:09:22 +01:00 committed by Mathieu Fenniak
commit 81baf75636
7 changed files with 255 additions and 10 deletions

View file

@ -533,3 +533,43 @@
updated: 1683636626
need_approval: false
approved_by: 0
-
id: 895
title: "job output"
repo_id: 4
owner_id: 1
workflow_id: "test.yaml"
index: 191
trigger_user_id: 1
ref: "refs/heads/master"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
is_fork_pull_request: false
status: 2
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: false
approved_by: 0
-
id: 896
title: "job output"
repo_id: 4
owner_id: 1
workflow_id: "test.yaml"
index: 192
trigger_user_id: 1
ref: "refs/heads/master"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
is_fork_pull_request: false
status: 2
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: false
approved_by: 0

View file

@ -69,6 +69,66 @@
status: 5
started: 1683636528
stopped: 1683636626
-
id: 197
run_id: 895
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
name: job1 (1)
attempt: 0
job_id: job1
task_id: 54
status: 2 # failure
runs_on: '["postmarketOS"]'
started: 1683636528
stopped: 1683636626
-
id: 198
run_id: 895
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
name: job1 (2)
attempt: 0
job_id: job1
task_id: 55
status: 6 # running
runs_on: '["postmarketOS"]'
started: 1683636528
stopped: 1683636626
-
id: 199
run_id: 896
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
name: job1 (1)
attempt: 0
job_id: job1
task_id: 56
status: 2 # failure
runs_on: '["postmarketOS"]'
started: 1683636528
stopped: 1683636626
-
id: 200
run_id: 896
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
name: job1 (2)
attempt: 0
job_id: job1
task_id: 57
status: 1 # success
runs_on: '["postmarketOS"]'
started: 1683636528
stopped: 1683636626
-
id: 292
run_id: 891

View file

@ -157,3 +157,83 @@
log_length: 707
log_size: 90179
log_expired: false
-
id: 54
job_id: 197
attempt: 0
runner_id: 1
status: 2 # failure
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784225
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: true
log_length: 707
log_size: 90179
log_expired: false
-
id: 55
job_id: 198
attempt: 0
runner_id: 1
status: 6 # running
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784226
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: true
log_length: 707
log_size: 90179
log_expired: false
-
id: 56
job_id: 199
attempt: 0
runner_id: 1
status: 2 # failure
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784227
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: true
log_length: 707
log_size: 90179
log_expired: false
-
id: 57
job_id: 200
attempt: 0
runner_id: 1
status: 1 # success
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: false
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784228
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: true
log_length: 707
log_size: 90179
log_expired: false

View file

@ -282,7 +282,6 @@ func getViewResponse(ctx *app_context.Context, req *ViewRequest, runIndex, jobIn
resp.State.Run.Title = run.Title
resp.State.Run.TitleHTML = templates.RenderCommitMessage(ctx, run.Title, metas)
resp.State.Run.Link = run.Link()
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
@ -310,6 +309,7 @@ func getViewResponse(ctx *app_context.Context, req *ViewRequest, runIndex, jobIn
})
}
resp.State.Run.Done = done
resp.State.Run.CanCancel = !done && ctx.Repo.CanWrite(unit.TypeActions)
pusher := ViewUser{
DisplayName: run.TriggerUser.GetDisplayName(),

View file

@ -382,6 +382,55 @@ func TestActionsViewViewPost(t *testing.T) {
}
}
func TestActionsViewCancelableUntilAllJobsFinished(t *testing.T) {
unittest.PrepareTestEnv(t)
tests := []struct {
name string
runIndex int64
assert func(*testing.T, *ViewResponse)
}{
{
name: "failed and running",
runIndex: 191,
assert: func(t *testing.T, actual *ViewResponse) {
assert.Equal(t, "failure", actual.State.Run.Jobs[0].Status)
assert.Equal(t, "running", actual.State.Run.Jobs[1].Status)
assert.True(t, actual.State.Run.CanCancel)
},
},
{
name: "failed and success",
runIndex: 192,
assert: func(t *testing.T, actual *ViewResponse) {
assert.Equal(t, "failure", actual.State.Run.Jobs[0].Status)
assert.Equal(t, "success", actual.State.Run.Jobs[1].Status)
assert.False(t, actual.State.Run.CanCancel)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "user2/repo1/actions/runs/0")
contexttest.LoadUser(t, ctx, 1)
contexttest.LoadRepo(t, ctx, 4)
ctx.SetParams(":run", fmt.Sprintf("%d", tt.runIndex))
ctx.SetParams(":attempt", fmt.Sprintf("%d", 0))
web.SetForm(ctx, &ViewRequest{})
ViewPost(ctx)
require.Equal(t, http.StatusOK, resp.Result().StatusCode, "failure in ViewPost(): %q", resp.Body.String())
var actual ViewResponse
err := json.Unmarshal(resp.Body.Bytes(), &actual)
require.NoError(t, err)
tt.assert(t, &actual)
})
}
}
func TestActionsViewRedirectToLatestAttempt(t *testing.T) {
unittest.PrepareTestEnv(t)

View file

@ -62,6 +62,7 @@ func TestActionsAPISearchActionJobs_GlobalRunnerAllPendingJobs(t *testing.T) {
defer tests.PrepareTestEnv(t)()
job196 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 196})
job198 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 198})
job393 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 393})
job394 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 394})
job395 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 395})
@ -81,11 +82,12 @@ func TestActionsAPISearchActionJobs_GlobalRunnerAllPendingJobs(t *testing.T) {
var jobs []*api.ActionRunJob
DecodeJSON(t, res, &jobs)
assert.Len(t, jobs, 6)
assert.Len(t, jobs, 7)
assert.Equal(t, job397.ID, jobs[0].ID)
assert.Equal(t, job396.ID, jobs[1].ID)
assert.Equal(t, job395.ID, jobs[2].ID)
assert.Equal(t, job394.ID, jobs[3].ID)
assert.Equal(t, job393.ID, jobs[4].ID)
assert.Equal(t, job196.ID, jobs[5].ID)
assert.Equal(t, job198.ID, jobs[5].ID)
assert.Equal(t, job196.ID, jobs[6].ID)
}

View file

@ -472,12 +472,14 @@ export default {
<button class="ui basic small compact button primary" @click="approveRun()" v-if="canApprove">
{{ locale.approve }}
</button>
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="canCancel">
{{ locale.cancel }}
</button>
<button class="ui basic small compact button tw-mr-0 tw-whitespace-nowrap link-action" :data-url="`${run.link}/rerun`" v-else-if="canRerun">
{{ locale.rerun_all }}
</button>
<div class="action-info-summary-actions" v-else>
<button class="ui basic small compact button red" @click="cancelRun()" v-if="canCancel">
{{ locale.cancel }}
</button>
<button class="ui basic small compact button tw-mr-0 tw-whitespace-nowrap link-action" :data-url="`${run.link}/rerun`" v-if="canRerun">
{{ locale.rerun_all }}
</button>
</div>
</div>
<div class="action-summary">
{{ run.commit.localeCommit }}
@ -629,9 +631,10 @@ export default {
.action-info-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.action-info-summary-title {
@ -640,6 +643,17 @@ export default {
gap: 0.5em;
}
.action-info-summary-actions {
display: flex;
align-items: center;
gap: var(--button-spacing);
margin-left: auto;
}
.action-info-summary-actions > button {
margin: 0;
}
.action-info-summary-title-text {
font-size: 20px;
margin: 0;