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:
parent
e79ff50560
commit
1454f9dafc
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user