Add actor and status dropdowns to run list (#25118)

Part of #25042

1. Added actor and status dropdowns first in case something is offtrack
and PR is too large.

2. Also added "No results matched." and "The workflow has no runs yet.",
and "No results matched." will show if there is no filter results and
there is no workflows (with [reference to github
action](https://github.com/go-gitea/gitea/actions/workflows/files-changed.yml?query=actor%3AGiteaBot))

Demo:


https://github.com/go-gitea/gitea/assets/17645053/6e76292c-4c1f-450d-8b48-99944cfc920c


TODOs:

- [x] Get available status (same as those in `aggregateJobStatus`)
instead of getting from database
- [x] Use `JOIN` to get actors, actors order by name
- [x] Make self on top
This commit is contained in:
HesterG 2023-06-21 12:25:14 +08:00 committed by GitHub
parent e79ff50560
commit 1454f9dafc
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 127 additions and 19 deletions

@ -71,6 +71,7 @@ type FindRunOptions struct {
WorkflowFileName string WorkflowFileName string
TriggerUserID int64 TriggerUserID int64
Approved bool // not util.OptionalBool, it works only when it's true Approved bool // not util.OptionalBool, it works only when it's true
Status Status
} }
func (opts FindRunOptions) toConds() builder.Cond { func (opts FindRunOptions) toConds() builder.Cond {
@ -90,6 +91,9 @@ func (opts FindRunOptions) toConds() builder.Cond {
if opts.Approved { if opts.Approved {
cond = cond.And(builder.Gt{"approved_by": 0}) cond = cond.And(builder.Gt{"approved_by": 0})
} }
if opts.Status > StatusUnknown {
cond = cond.And(builder.Eq{"status": opts.Status})
}
return cond return cond
} }
@ -106,3 +110,34 @@ func FindRuns(ctx context.Context, opts FindRunOptions) (RunList, int64, error)
func CountRuns(ctx context.Context, opts FindRunOptions) (int64, error) { func CountRuns(ctx context.Context, opts FindRunOptions) (int64, error) {
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionRun)) return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionRun))
} }
type StatusInfo struct {
Status int
DisplayedStatus string
}
// GetStatusInfoList returns a slice of StatusInfo
func GetStatusInfoList(ctx context.Context) []StatusInfo {
// same as those in aggregateJobStatus
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning}
statusInfoList := make([]StatusInfo, 0, 4)
for _, s := range allStatus {
statusInfoList = append(statusInfoList, StatusInfo{
Status: int(s),
DisplayedStatus: s.String(),
})
}
return statusInfoList
}
// GetActors returns a slice of Actors
func GetActors(ctx context.Context, repoID int64) ([]*user_model.User, error) {
actors := make([]*user_model.User, 0, 10)
return actors, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`action_run`.trigger_user_id").From("`action_run`").
GroupBy("`action_run`.trigger_user_id").
Where(builder.Eq{"`action_run`.repo_id": repoID}))).
Cols("id", "name", "full_name", "avatar", "avatar_email", "use_custom_avatar").
OrderBy(user_model.GetOrderByName()).
Find(&actors)
}

@ -3460,6 +3460,12 @@ runs.commit = Commit
runs.pushed_by = Pushed by runs.pushed_by = Pushed by
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
runs.no_matching_runner_helper = No matching runner: %s runs.no_matching_runner_helper = No matching runner: %s
runs.actor = Actor
runs.status = Status
runs.actors_no_select = All actors
runs.status_no_select = All status
runs.no_results = No results matched.
runs.no_runs = The workflow has no runs yet.
need_approval_desc = Need approval to run workflows for fork pull request. need_approval_desc = Need approval to run workflows for fork pull request.

@ -5,6 +5,7 @@ package actions
import ( import (
"bytes" "bytes"
"fmt"
"net/http" "net/http"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
@ -125,7 +127,16 @@ func List(ctx *context.Context) {
} }
workflow := ctx.FormString("workflow") workflow := ctx.FormString("workflow")
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
ctx.Data["CurWorkflow"] = workflow ctx.Data["CurWorkflow"] = workflow
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
// they will be 0 by default, which indicates get all status or actors
ctx.Data["CurActor"] = actorID
ctx.Data["CurStatus"] = status
if actorID > 0 || status > int(actions_model.StatusUnknown) {
ctx.Data["IsFiltered"] = true
}
opts := actions_model.FindRunOptions{ opts := actions_model.FindRunOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
@ -134,6 +145,8 @@ func List(ctx *context.Context) {
}, },
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
WorkflowFileName: workflow, WorkflowFileName: workflow,
TriggerUserID: actorID,
Status: actions_model.Status(status),
} }
runs, total, err := actions_model.FindRuns(ctx, opts) runs, total, err := actions_model.FindRuns(ctx, opts)
@ -153,9 +166,20 @@ func List(ctx *context.Context) {
ctx.Data["Runs"] = runs ctx.Data["Runs"] = runs
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx, actors)
ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx)
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx) pager.SetDefaultParams(ctx)
pager.AddParamString("workflow", workflow) pager.AddParamString("workflow", workflow)
pager.AddParamString("actor", fmt.Sprint(actorID))
pager.AddParamString("status", fmt.Sprint(status))
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplListActions) ctx.HTML(http.StatusOK, tplListActions)

@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
) )
func makeSelfOnTop(ctx *context.Context, users []*user.User) []*user.User { func MakeSelfOnTop(ctx *context.Context, users []*user.User) []*user.User {
if ctx.Doer != nil { if ctx.Doer != nil {
sort.Slice(users, func(i, j int) bool { sort.Slice(users, func(i, j int) bool {
if users[i].ID == users[j].ID { if users[i].ID == users[j].ID {

@ -13,15 +13,15 @@ import (
) )
func TestMakeSelfOnTop(t *testing.T) { func TestMakeSelfOnTop(t *testing.T) {
users := makeSelfOnTop(&context.Context{}, []*user.User{{ID: 2}, {ID: 1}}) users := MakeSelfOnTop(&context.Context{}, []*user.User{{ID: 2}, {ID: 1}})
assert.Len(t, users, 2) assert.Len(t, users, 2)
assert.EqualValues(t, 2, users[0].ID) assert.EqualValues(t, 2, users[0].ID)
users = makeSelfOnTop(&context.Context{Doer: &user.User{ID: 1}}, []*user.User{{ID: 2}, {ID: 1}}) users = MakeSelfOnTop(&context.Context{Doer: &user.User{ID: 1}}, []*user.User{{ID: 2}, {ID: 1}})
assert.Len(t, users, 2) assert.Len(t, users, 2)
assert.EqualValues(t, 1, users[0].ID) assert.EqualValues(t, 1, users[0].ID)
users = makeSelfOnTop(&context.Context{Doer: &user.User{ID: 2}}, []*user.User{{ID: 2}, {ID: 1}}) users = MakeSelfOnTop(&context.Context{Doer: &user.User{ID: 2}}, []*user.User{{ID: 2}, {ID: 1}})
assert.Len(t, users, 2) assert.Len(t, users, 2)
assert.EqualValues(t, 2, users[0].ID) assert.EqualValues(t, 2, users[0].ID)
} }

@ -312,7 +312,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ctx.ServerError("GetRepoAssignees", err) ctx.ServerError("GetRepoAssignees", err)
return return
} }
ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
handleTeamMentions(ctx) handleTeamMentions(ctx)
if ctx.Written() { if ctx.Written() {
@ -508,7 +508,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
ctx.ServerError("GetRepoAssignees", err) ctx.ServerError("GetRepoAssignees", err)
return return
} }
ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
handleTeamMentions(ctx) handleTeamMentions(ctx)
} }
@ -3487,7 +3487,7 @@ func IssuePosters(ctx *context.Context) {
} }
} }
posters = makeSelfOnTop(ctx, posters) posters = MakeSelfOnTop(ctx, posters)
resp := &userSearchResponse{} resp := &userSearchResponse{}
resp.Results = make([]*userSearchInfo, len(posters)) resp.Results = make([]*userSearchInfo, len(posters))

@ -809,7 +809,7 @@ func ViewPullFiles(ctx *context.Context) {
ctx.ServerError("GetRepoAssignees", err) ctx.ServerError("GetRepoAssignees", err)
return return
} }
ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
handleTeamMentions(ctx) handleTeamMentions(ctx)
if ctx.Written() { if ctx.Written() {

@ -349,7 +349,7 @@ func NewRelease(ctx *context.Context) {
ctx.ServerError("GetRepoAssignees", err) ctx.ServerError("GetRepoAssignees", err)
return return
} }
ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
upload.AddUploadContext(ctx, "release") upload.AddUploadContext(ctx, "release")
ctx.HTML(http.StatusOK, tplReleaseNew) ctx.HTML(http.StatusOK, tplReleaseNew)
@ -517,7 +517,7 @@ func EditRelease(ctx *context.Context) {
ctx.ServerError("GetRepoAssignees", err) ctx.ServerError("GetRepoAssignees", err)
return return
} }
ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
ctx.HTML(http.StatusOK, tplReleaseNew) ctx.HTML(http.StatusOK, tplReleaseNew)
} }

@ -38,7 +38,7 @@
{{else}} {{else}}
{{if not .HasPackages}} {{if not .HasPackages}}
<div class="empty center"> <div class="empty center">
{{svg "octicon-package" 32}} {{svg "octicon-package" 48}}
<h2>{{.locale.Tr "packages.empty"}}</h2> <h2>{{.locale.Tr "packages.empty"}}</h2>
{{if and .Repository .CanWritePackages}} {{if and .Repository .CanWritePackages}}
{{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}} {{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}}

@ -1,14 +1,14 @@
{{template "base/head" .}} {{template "base/head" .}}
<div class="page-content repository"> <div class="page-content repository actions">
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
<div class="ui stackable grid"> <div class="ui stackable grid">
<div class="four wide column"> <div class="four wide column">
<div class="ui fluid vertical menu"> <div class="ui fluid vertical menu">
<a class="item{{if not $.CurWorkflow}} active{{end}}" href="{{$.Link}}">{{.locale.Tr "actions.runs.all_workflows"}}</a> <a class="item{{if not $.CurWorkflow}} active{{end}}" href="{{$.Link}}?actor={{$.CurActor}}&status={{$.CurStatus}}">{{.locale.Tr "actions.runs.all_workflows"}}</a>
<div class="divider"></div> <div class="divider"></div>
{{range .workflows}} {{range .workflows}}
<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="{{$.Link}}?workflow={{.Entry.Name}}">{{.Entry.Name}} <a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="{{$.Link}}?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
{{if .ErrMsg}} {{if .ErrMsg}}
<span data-tooltip-content="{{.ErrMsg}}"> <span data-tooltip-content="{{.ErrMsg}}">
{{svg "octicon-alert" 16 "text red"}} {{svg "octicon-alert" 16 "text red"}}
@ -19,6 +19,46 @@
</div> </div>
</div> </div>
<div class="twelve wide column content"> <div class="twelve wide column content">
<div class="ui secondary filter stackable menu gt-je">
<!-- Actor -->
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
<span class="text">{{.locale.Tr "actions.runs.actor"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search"}}</i>
<input type="text" placeholder="{{.locale.Tr "actions.runs.actor"}}">
</div>
<a class="item{{if not $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
{{.locale.Tr "actions.runs.actors_no_select"}}
</a>
{{range .Actors}}
<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
{{avatar $.Context . 20}} {{.GetDisplayName}}
</a>
{{end}}
</div>
</div>
<!-- Status -->
<div class="ui dropdown jump item">
<span class="text">{{.locale.Tr "actions.runs.status"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search"}}</i>
<input type="text" placeholder="{{.locale.Tr "actions.runs.status"}}">
</div>
<a class="item{{if not $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
{{.locale.Tr "actions.runs.status_no_select"}}
</a>
{{range .StatusInfoList}}
<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
{{.DisplayedStatus}}
</a>
{{end}}
</div>
</div>
</div>
{{template "repo/actions/runs_list" .}} {{template "repo/actions/runs_list" .}}
</div> </div>
</div> </div>

@ -1,4 +1,10 @@
<div class="issue list gt-m-0"> <div class="issue list gt-m-0">
{{if eq (len .Runs) 0}}
<div class="empty center">
{{svg "octicon-no-entry" 48}}
<h2>{{if $.IsFiltered}}{{.locale.Tr "actions.runs.no_results"}}{{else}}{{.locale.Tr "actions.runs.no_runs"}}{{end}}</h2>
</div>
{{end}}
{{range .Runs}} {{range .Runs}}
<li class="item gt-df gt-py-4"> <li class="item gt-df gt-py-4">
<div class="issue-item-left issue-item-icon gt-df gt-items-start"> <div class="issue-item-left issue-item-icon gt-df gt-items-start">

@ -1877,15 +1877,12 @@
flex: 1 flex: 1
} }
.repository.packages .empty { .repository.packages .empty,
.repository.actions .empty {
padding-top: 70px; padding-top: 70px;
padding-bottom: 100px; padding-bottom: 100px;
} }
.repository.packages .empty .svg {
height: 48px;
}
.repository.packages .file-size { .repository.packages .file-size {
white-space: nowrap; white-space: nowrap;
} }