[v14.0/forgejo] feat(ui): show cancel button until all jobs are finished (#10531)

**Backport:** https://codeberg.org/forgejo/forgejo/pulls/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.

Fixes #8922

Co-authored-by: Beowulf <beowulf@beocode.eu>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10531
Reviewed-by: Beowulf <beowulf@beocode.eu>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
This commit is contained in:
forgejo-backport-action 2025-12-21 19:18:37 +01:00 committed by Beowulf
commit dd75d0957d
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;