mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-19 15:14:05 -05:00
enhance(actions): Make Summary UI more beautiful with more infos (#37824)
## Summary - Redesign the Actions run summary header to follow GitHub Actions layout: trigger info on the left, Status / Total duration / Artifacts columns inline on the right - Expose trigger user avatar, pull request link, and PR head branch info from the run view API - Update the workflow graph header to show the workflow filename (linked to the run workflow file) and `on: <event>`, while keeping the jobs/dependencies/success stats line - Remove the redundant commit/workflow metadata row below the run title; that information now lives in the summary bar New: <img width="1564" height="639" src="https://github.com/user-attachments/assets/e6bc1623-c5fc-4e97-abc9-fde7f3c6aef9" /> Old: <img width="2038" height="1038" src="https://github.com/user-attachments/assets/0857f19a-8d3a-4da2-82fd-e9ebeb200062" /> Replaces https://github.com/go-gitea/gitea/pull/36721 --------- Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
@@ -3804,7 +3804,10 @@
|
|||||||
"actions.runs.latest": "Latest",
|
"actions.runs.latest": "Latest",
|
||||||
"actions.runs.latest_attempt": "Latest attempt",
|
"actions.runs.latest_attempt": "Latest attempt",
|
||||||
"actions.runs.triggered_via": "Triggered via %s",
|
"actions.runs.triggered_via": "Triggered via %s",
|
||||||
"actions.runs.total_duration": "Total duration:",
|
"actions.runs.rerun_triggered": "Re-run triggered",
|
||||||
|
"actions.runs.back_to_pull_request": "Back to pull request",
|
||||||
|
"actions.runs.back_to_workflow": "Back to workflow",
|
||||||
|
"actions.runs.total_duration": "Total duration",
|
||||||
"actions.runs.workflow_dependencies": "Workflow Dependencies",
|
"actions.runs.workflow_dependencies": "Workflow Dependencies",
|
||||||
"actions.runs.graph_jobs_count_1": "%d job",
|
"actions.runs.graph_jobs_count_1": "%d job",
|
||||||
"actions.runs.graph_jobs_count_n": "%d jobs",
|
"actions.runs.graph_jobs_count_n": "%d jobs",
|
||||||
|
|||||||
@@ -87,29 +87,39 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
|||||||
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
|
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
|
||||||
resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10)
|
resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10)
|
||||||
resp.State.Run.CanDeleteArtifact = true
|
resp.State.Run.CanDeleteArtifact = true
|
||||||
resp.State.Run.WorkflowID = "workflow-id"
|
resp.State.Run.WorkflowID = "workflow-id.yml"
|
||||||
resp.State.Run.WorkflowLink = "./workflow-link"
|
|
||||||
resp.State.Run.TriggerEvent = "push"
|
resp.State.Run.TriggerEvent = "push"
|
||||||
|
user2, _ := user_model.GetUserByID(ctx, 2)
|
||||||
|
if user2 == nil {
|
||||||
|
user2 = &user_model.User{Name: "user2"}
|
||||||
|
}
|
||||||
|
user3, _ := user_model.GetUserByID(ctx, 3)
|
||||||
|
if user3 == nil {
|
||||||
|
user3 = &user_model.User{Name: "user3"}
|
||||||
|
}
|
||||||
resp.State.Run.Commit = actions.ViewCommit{
|
resp.State.Run.Commit = actions.ViewCommit{
|
||||||
ShortSha: "ccccdddd",
|
ShortSha: "ccccdddd",
|
||||||
Link: "./commit-link",
|
Link: "./commit-link",
|
||||||
Pusher: actions.ViewUser{
|
Pusher: actions.ViewUser{
|
||||||
DisplayName: "pusher user",
|
DisplayName: user2.GetDisplayName(),
|
||||||
Link: "./pusher-link",
|
Link: user2.HomeLink(),
|
||||||
|
AvatarLink: user2.AvatarLinkWithSize(ctx, 16),
|
||||||
},
|
},
|
||||||
Branch: actions.ViewBranch{
|
Branch: actions.ViewBranch{
|
||||||
Name: "commit-branch",
|
Name: "user2:commit-branch",
|
||||||
Link: "./branch-link",
|
Link: "./branch-link",
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
resp.State.Run.PullRequest = &actions.ViewPullRequest{
|
||||||
|
Index: "#37658",
|
||||||
|
Link: "./pull/37658",
|
||||||
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
currentAttemptNum := int64(1)
|
currentAttemptNum := int64(1)
|
||||||
if attemptID > 0 {
|
if attemptID > 0 {
|
||||||
currentAttemptNum = attemptID
|
currentAttemptNum = attemptID
|
||||||
}
|
}
|
||||||
user2 := &user_model.User{Name: "user2"}
|
|
||||||
user3 := &user_model.User{Name: "user3"}
|
|
||||||
attempts := []*actions_model.ActionRunAttempt{{
|
attempts := []*actions_model.ActionRunAttempt{{
|
||||||
Attempt: 1,
|
Attempt: 1,
|
||||||
Status: actions_model.StatusSuccess,
|
Status: actions_model.StatusSuccess,
|
||||||
@@ -177,6 +187,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
|||||||
TriggeredAt: attempt.Created.AsTime().Unix(),
|
TriggeredAt: attempt.Created.AsTime().Unix(),
|
||||||
TriggerUserName: attempt.TriggerUser.GetDisplayName(),
|
TriggerUserName: attempt.TriggerUser.GetDisplayName(),
|
||||||
TriggerUserLink: attempt.TriggerUser.HomeLink(),
|
TriggerUserLink: attempt.TriggerUser.HomeLink(),
|
||||||
|
TriggerUserAvatar: attempt.TriggerUser.AvatarLinkWithSize(ctx, 16),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
isLatestAttempt := currentAttemptNum == latestAttempt.Attempt
|
isLatestAttempt := currentAttemptNum == latestAttempt.Attempt
|
||||||
|
|||||||
@@ -20,14 +20,18 @@ import (
|
|||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
"gitea.dev/modules/actions"
|
"gitea.dev/modules/actions"
|
||||||
"gitea.dev/modules/base"
|
"gitea.dev/modules/base"
|
||||||
|
"gitea.dev/modules/cache"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/httplib"
|
"gitea.dev/modules/httplib"
|
||||||
|
"gitea.dev/modules/json"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/storage"
|
"gitea.dev/modules/storage"
|
||||||
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/templates"
|
"gitea.dev/modules/templates"
|
||||||
"gitea.dev/modules/translation"
|
"gitea.dev/modules/translation"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
@@ -306,6 +310,7 @@ type ViewResponse struct {
|
|||||||
Attempts []*ViewRunAttempt `json:"attempts"`
|
Attempts []*ViewRunAttempt `json:"attempts"`
|
||||||
Jobs []*ViewJob `json:"jobs"`
|
Jobs []*ViewJob `json:"jobs"`
|
||||||
Commit ViewCommit `json:"commit"`
|
Commit ViewCommit `json:"commit"`
|
||||||
|
PullRequest *ViewPullRequest `json:"pullRequest,omitempty"`
|
||||||
// Summary view: run duration and trigger time/event
|
// Summary view: run duration and trigger time/event
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
|
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
|
||||||
@@ -349,6 +354,12 @@ type ViewRunAttempt struct {
|
|||||||
TriggeredAt int64 `json:"triggeredAt"`
|
TriggeredAt int64 `json:"triggeredAt"`
|
||||||
TriggerUserName string `json:"triggerUserName"`
|
TriggerUserName string `json:"triggerUserName"`
|
||||||
TriggerUserLink string `json:"triggerUserLink"`
|
TriggerUserLink string `json:"triggerUserLink"`
|
||||||
|
TriggerUserAvatar string `json:"triggerUserAvatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewPullRequest struct {
|
||||||
|
Index string `json:"index"`
|
||||||
|
Link string `json:"link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewCommit struct {
|
type ViewCommit struct {
|
||||||
@@ -361,6 +372,7 @@ type ViewCommit struct {
|
|||||||
type ViewUser struct {
|
type ViewUser struct {
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
|
AvatarLink string `json:"avatarLink,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewBranch struct {
|
type ViewBranch struct {
|
||||||
@@ -388,6 +400,132 @@ type ViewStepLogLine struct {
|
|||||||
Timestamp float64 `json:"timestamp"`
|
Timestamp float64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func viewPullRequestFromRun(ctx context.Context, run *actions_model.ActionRun, prPayload *api.PullRequestPayload) *ViewPullRequest {
|
||||||
|
if run.Repo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
refName := git.RefName(run.Ref)
|
||||||
|
if refName.IsPull() {
|
||||||
|
return &ViewPullRequest{
|
||||||
|
Index: "#" + refName.ShortName(),
|
||||||
|
Link: run.RefLink(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if prPayload != nil && prPayload.Index > 0 {
|
||||||
|
return &ViewPullRequest{
|
||||||
|
Index: fmt.Sprintf("#%d", prPayload.Index),
|
||||||
|
Link: fmt.Sprintf("%s/pulls/%d", run.Repo.Link(), prPayload.Index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Push-triggered run: surface an open PR whose head matches this branch so
|
||||||
|
// users coming from a PR's check details can navigate back to it.
|
||||||
|
if refName.IsBranch() {
|
||||||
|
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, run.RepoID, refName.ShortName())
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetUnmergedPullRequestsByHeadInfo: %v", err)
|
||||||
|
} else if len(prs) == 1 {
|
||||||
|
pr := prs[0]
|
||||||
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||||
|
log.Error("LoadBaseRepo: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ViewPullRequest{
|
||||||
|
Index: fmt.Sprintf("#%d", pr.Index),
|
||||||
|
Link: fmt.Sprintf("%s/pulls/%d", pr.BaseRepo.Link(), pr.Index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewSummaryBranchFromRun(ctx context.Context, run *actions_model.ActionRun, prPayload *api.PullRequestPayload) ViewBranch {
|
||||||
|
refName := git.RefName(run.Ref)
|
||||||
|
if prPayload != nil && prPayload.PullRequest != nil && prPayload.PullRequest.Head != nil {
|
||||||
|
head := prPayload.PullRequest.Head
|
||||||
|
name := head.Name
|
||||||
|
if name == "" {
|
||||||
|
name = git.RefName(head.Ref).ShortName()
|
||||||
|
}
|
||||||
|
if head.Repository != nil && run.Repo != nil && head.RepoID > 0 && head.RepoID != run.Repo.ID {
|
||||||
|
ownerName := ""
|
||||||
|
if head.Repository.Owner != nil {
|
||||||
|
ownerName = head.Repository.Owner.UserName
|
||||||
|
} else if head.Repository.FullName != "" {
|
||||||
|
ownerName, _, _ = strings.Cut(head.Repository.FullName, "/")
|
||||||
|
}
|
||||||
|
if ownerName != "" && !strings.Contains(name, ":") {
|
||||||
|
name = ownerName + ":" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
link := ""
|
||||||
|
if head.Repository != nil && head.Ref != "" {
|
||||||
|
repoLink := head.Repository.Link
|
||||||
|
if repoLink == "" {
|
||||||
|
repoLink = head.Repository.HTMLURL
|
||||||
|
}
|
||||||
|
if repoLink != "" {
|
||||||
|
link = repoLink + "/src/" + git.RefName(head.Ref).RefWebLinkPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ViewBranch{Name: name, Link: link}
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := ViewBranch{
|
||||||
|
Name: run.PrettyRef(),
|
||||||
|
Link: run.RefLink(),
|
||||||
|
}
|
||||||
|
if refName.IsBranch() {
|
||||||
|
b, err := git_model.GetBranch(ctx, run.RepoID, refName.ShortName())
|
||||||
|
if err != nil && !git_model.IsErrBranchNotExist(err) {
|
||||||
|
log.Error("GetBranch: %v", err)
|
||||||
|
} else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) {
|
||||||
|
branch.IsDeleted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionsSummaryRefCacheTTL bounds how long the resolved PR/branch summary is
|
||||||
|
// cached. ViewPost is polled every second, but this metadata is stable for a
|
||||||
|
// run, so a short TTL collapses the repeated DB lookups while staying fresh
|
||||||
|
// enough for the navigation links.
|
||||||
|
const actionsSummaryRefCacheTTL = 10 // seconds
|
||||||
|
|
||||||
|
type viewSummaryRefInfo struct {
|
||||||
|
PullRequest *ViewPullRequest `json:"pullRequest"`
|
||||||
|
Branch ViewBranch `json:"branch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getViewSummaryRefInfo resolves the run's pull request and head branch summary,
|
||||||
|
// caching the result briefly so the per-second poll does not hit the database on
|
||||||
|
// every request (GetUnmergedPullRequestsByHeadInfo / GetBranch).
|
||||||
|
func getViewSummaryRefInfo(ctx context.Context, run *actions_model.ActionRun) viewSummaryRefInfo {
|
||||||
|
compute := func() viewSummaryRefInfo {
|
||||||
|
// parse the event payload once and share it between both resolvers
|
||||||
|
prPayload, _ := run.GetPullRequestEventPayload() // nil unless this is a pull request event
|
||||||
|
return viewSummaryRefInfo{
|
||||||
|
PullRequest: viewPullRequestFromRun(ctx, run, prPayload),
|
||||||
|
Branch: viewSummaryBranchFromRun(ctx, run, prPayload),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c := cache.GetCache()
|
||||||
|
if c == nil {
|
||||||
|
return compute()
|
||||||
|
}
|
||||||
|
cacheKey := fmt.Sprintf("actions_run_summary_ref:%d", run.ID)
|
||||||
|
if cached, ok := c.Get(cacheKey); ok && cached != "" {
|
||||||
|
var info viewSummaryRefInfo
|
||||||
|
if err := json.Unmarshal([]byte(cached), &info); err == nil {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info := compute()
|
||||||
|
if data, err := json.Marshal(info); err == nil {
|
||||||
|
_ = c.Put(cacheKey, string(data), actionsSummaryRefCacheTTL)
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
func ViewPost(ctx *context_module.Context) {
|
func ViewPost(ctx *context_module.Context) {
|
||||||
run, attempt, jobs := getCurrentRunJobsByPathParam(ctx)
|
run, attempt, jobs := getCurrentRunJobsByPathParam(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
@@ -491,33 +629,24 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
|
|||||||
TriggeredAt: runAttempt.Created.AsTime().Unix(),
|
TriggeredAt: runAttempt.Created.AsTime().Unix(),
|
||||||
TriggerUserName: runAttempt.TriggerUser.GetDisplayName(),
|
TriggerUserName: runAttempt.TriggerUser.GetDisplayName(),
|
||||||
TriggerUserLink: runAttempt.TriggerUser.HomeLink(),
|
TriggerUserLink: runAttempt.TriggerUser.HomeLink(),
|
||||||
|
TriggerUserAvatar: runAttempt.TriggerUser.AvatarLinkWithSize(ctx, 16),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pusher := ViewUser{
|
pusher := ViewUser{
|
||||||
DisplayName: run.TriggerUser.GetDisplayName(),
|
DisplayName: run.TriggerUser.GetDisplayName(),
|
||||||
Link: run.TriggerUser.HomeLink(),
|
Link: run.TriggerUser.HomeLink(),
|
||||||
}
|
AvatarLink: run.TriggerUser.AvatarLinkWithSize(ctx, 16),
|
||||||
branch := ViewBranch{
|
|
||||||
Name: run.PrettyRef(),
|
|
||||||
Link: run.RefLink(),
|
|
||||||
}
|
|
||||||
refName := git.RefName(run.Ref)
|
|
||||||
if refName.IsBranch() {
|
|
||||||
b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName())
|
|
||||||
if err != nil && !git_model.IsErrBranchNotExist(err) {
|
|
||||||
log.Error("GetBranch: %v", err)
|
|
||||||
} else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) {
|
|
||||||
branch.IsDeleted = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refInfo := getViewSummaryRefInfo(ctx, run)
|
||||||
resp.State.Run.Commit = ViewCommit{
|
resp.State.Run.Commit = ViewCommit{
|
||||||
ShortSha: base.ShortSha(run.CommitSHA),
|
ShortSha: base.ShortSha(run.CommitSHA),
|
||||||
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
|
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
|
||||||
Pusher: pusher,
|
Pusher: pusher,
|
||||||
Branch: branch,
|
Branch: refInfo.Branch,
|
||||||
}
|
}
|
||||||
|
resp.State.Run.PullRequest = refInfo.PullRequest
|
||||||
resp.State.Run.TriggerEvent = run.TriggerEvent
|
resp.State.Run.TriggerEvent = run.TriggerEvent
|
||||||
|
|
||||||
// Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts all share run_attempt_id=0,
|
// Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts all share run_attempt_id=0,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
"gitea.dev/modules/translation"
|
"gitea.dev/modules/translation"
|
||||||
|
|
||||||
@@ -14,6 +16,66 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestViewPullRequestFromRun(t *testing.T) {
|
||||||
|
repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"}
|
||||||
|
|
||||||
|
t.Run("pull ref", func(t *testing.T) {
|
||||||
|
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/123/head"}
|
||||||
|
assert.Equal(t, &ViewPullRequest{Index: "#123", Link: "/owner/repo/pulls/123"}, viewPullRequestFromRun(t.Context(), run, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pull request event payload", func(t *testing.T) {
|
||||||
|
// a non-pull ref forces the payload branch instead of the ref branch
|
||||||
|
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/heads/feature"}
|
||||||
|
payload := &api.PullRequestPayload{Index: 42}
|
||||||
|
assert.Equal(t, &ViewPullRequest{Index: "#42", Link: "/owner/repo/pulls/42"}, viewPullRequestFromRun(t.Context(), run, payload))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil repo", func(t *testing.T) {
|
||||||
|
run := &actions_model.ActionRun{Ref: "refs/pull/1/head"}
|
||||||
|
assert.Nil(t, viewPullRequestFromRun(t.Context(), run, nil))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestViewSummaryBranchFromRun(t *testing.T) {
|
||||||
|
repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"}
|
||||||
|
|
||||||
|
t.Run("pull request event same repo", func(t *testing.T) {
|
||||||
|
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"}
|
||||||
|
payload := &api.PullRequestPayload{
|
||||||
|
PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{
|
||||||
|
Name: "feature",
|
||||||
|
Ref: "refs/heads/feature",
|
||||||
|
RepoID: 1,
|
||||||
|
Repository: &api.Repository{Link: "/owner/repo"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ViewBranch{Name: "feature", Link: "/owner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pull request event from fork prefixes owner", func(t *testing.T) {
|
||||||
|
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"}
|
||||||
|
payload := &api.PullRequestPayload{
|
||||||
|
PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{
|
||||||
|
Name: "feature",
|
||||||
|
Ref: "refs/heads/feature",
|
||||||
|
RepoID: 2,
|
||||||
|
Repository: &api.Repository{
|
||||||
|
Link: "/forkowner/repo",
|
||||||
|
Owner: &api.User{UserName: "forkowner"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ViewBranch{Name: "forkowner:feature", Link: "/forkowner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("push to tag does not query branch", func(t *testing.T) {
|
||||||
|
// a tag ref is not a branch, so no GetBranch DB lookup happens
|
||||||
|
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/tags/v1.0.0"}
|
||||||
|
assert.Equal(t, ViewBranch{Name: "v1.0.0", Link: "/owner/repo/src/tag/v1.0.0"}, viewSummaryBranchFromRun(t.Context(), run, nil))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestConvertToViewModel(t *testing.T) {
|
func TestConvertToViewModel(t *testing.T) {
|
||||||
task := &actions_model.ActionTask{
|
task := &actions_model.ActionTask{
|
||||||
Status: actions_model.StatusSuccess,
|
Status: actions_model.StatusSuccess,
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
data-locale-expand-caller-jobs="{{ctx.Locale.Tr "actions.runs.expand_caller_jobs"}}"
|
data-locale-expand-caller-jobs="{{ctx.Locale.Tr "actions.runs.expand_caller_jobs"}}"
|
||||||
data-locale-collapse-caller-jobs="{{ctx.Locale.Tr "actions.runs.collapse_caller_jobs"}}"
|
data-locale-collapse-caller-jobs="{{ctx.Locale.Tr "actions.runs.collapse_caller_jobs"}}"
|
||||||
data-locale-triggered-via="{{ctx.Locale.Tr "actions.runs.triggered_via"}}"
|
data-locale-triggered-via="{{ctx.Locale.Tr "actions.runs.triggered_via"}}"
|
||||||
|
data-locale-rerun-triggered="{{ctx.Locale.Tr "actions.runs.rerun_triggered"}}"
|
||||||
|
data-locale-back-to-pull-request="{{ctx.Locale.Tr "actions.runs.back_to_pull_request"}}"
|
||||||
|
data-locale-back-to-workflow="{{ctx.Locale.Tr "actions.runs.back_to_workflow"}}"
|
||||||
|
data-locale-status-label="{{ctx.Locale.Tr "actions.runs.status"}}"
|
||||||
data-locale-total-duration="{{ctx.Locale.Tr "actions.runs.total_duration"}}"
|
data-locale-total-duration="{{ctx.Locale.Tr "actions.runs.total_duration"}}"
|
||||||
data-locale-run-details="{{ctx.Locale.Tr "actions.runs.run_details"}}"
|
data-locale-run-details="{{ctx.Locale.Tr "actions.runs.run_details"}}"
|
||||||
data-locale-workflow-file="{{ctx.Locale.Tr "actions.runs.workflow_file"}}"
|
data-locale-workflow-file="{{ctx.Locale.Tr "actions.runs.workflow_file"}}"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ActionStatusIcon from './ActionStatusIcon.vue';
|
|
||||||
import WorkflowGraph from './WorkflowGraph.vue';
|
import WorkflowGraph from './WorkflowGraph.vue';
|
||||||
import type {ActionRunViewStore} from "./ActionRunView.ts";
|
import type {ActionRunViewStore} from "./ActionRunView.ts";
|
||||||
import {computed, onBeforeUnmount, onMounted, toRefs} from "vue";
|
import {computed, onBeforeUnmount, onMounted, toRefs} from "vue";
|
||||||
@@ -11,6 +10,7 @@ defineOptions({
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
store: ActionRunViewStore;
|
store: ActionRunViewStore;
|
||||||
locale: Record<string, any>;
|
locale: Record<string, any>;
|
||||||
|
artifactCount: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const locale = props.locale;
|
const locale = props.locale;
|
||||||
@@ -25,12 +25,27 @@ const topLevelJobs = computed(() => (run.value.jobs || []).filter((j) => !j.pare
|
|||||||
const triggerUser = computed(() => {
|
const triggerUser = computed(() => {
|
||||||
const currentAttempt = run.value.attempts.find((attempt) => attempt.current);
|
const currentAttempt = run.value.attempts.find((attempt) => attempt.current);
|
||||||
if (currentAttempt) {
|
if (currentAttempt) {
|
||||||
return {name: currentAttempt.triggerUserName, link: currentAttempt.triggerUserLink};
|
return {
|
||||||
|
name: currentAttempt.triggerUserName,
|
||||||
|
link: currentAttempt.triggerUserLink,
|
||||||
|
avatar: currentAttempt.triggerUserAvatar,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const pusher = run.value.commit.pusher;
|
const pusher = run.value.commit.pusher;
|
||||||
return pusher.displayName ? {name: pusher.displayName, link: pusher.link} : null;
|
return pusher.displayName ? {
|
||||||
|
name: pusher.displayName,
|
||||||
|
link: pusher.link,
|
||||||
|
avatar: pusher.avatarLink,
|
||||||
|
} : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const triggerLabel = computed(() => {
|
||||||
|
if (isRerun.value) return locale.rerunTriggered;
|
||||||
|
return locale.triggeredVia.replace('%s', run.value.triggerEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
const artifactsDisplay = computed(() => props.artifactCount > 0 ? String(props.artifactCount) : '–');
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await props.store.startPollingCurrentRun();
|
await props.store.startPollingCurrentRun();
|
||||||
});
|
});
|
||||||
@@ -42,19 +57,60 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-run-summary-view">
|
<div class="action-run-summary-view">
|
||||||
<div class="action-run-summary-block">
|
<div class="action-run-summary-block">
|
||||||
<div class="flex-text-block">
|
<div class="action-run-summary-trigger">
|
||||||
<span>{{ isRerun ? locale.rerun : locale.triggeredVia.replace('%s', run.triggerEvent) }}</span>
|
<span class="action-run-summary-label">
|
||||||
<template v-if="triggerUser">
|
{{ triggerLabel }} <relative-time :datetime="run.triggeredAt || ''" prefix=""/>
|
||||||
<span>•</span>
|
</span>
|
||||||
<a v-if="triggerUser.link" class="muted" :href="triggerUser.link">{{ triggerUser.name }}</a>
|
<div class="flex-text-block tw-flex-wrap action-run-summary-trigger-content">
|
||||||
<span v-else class="muted">{{ triggerUser.name }}</span>
|
<component
|
||||||
</template>
|
:is="triggerUser.link ? 'a' : 'span'"
|
||||||
<span>•</span>
|
v-if="triggerUser"
|
||||||
<relative-time :datetime="run.triggeredAt || ''" prefix=""/>
|
class="flex-text-inline action-run-summary-user"
|
||||||
|
:class="{silenced: triggerUser.link}"
|
||||||
|
:href="triggerUser.link || undefined"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="triggerUser.avatar"
|
||||||
|
class="ui avatar tw-align-middle"
|
||||||
|
:src="triggerUser.avatar"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
:alt="triggerUser.name"
|
||||||
|
>
|
||||||
|
<span>{{ triggerUser.name }}</span>
|
||||||
|
</component>
|
||||||
|
<a v-if="run.pullRequest" class="action-run-summary-pr silenced" :href="run.pullRequest.link">{{ run.pullRequest.index }}</a>
|
||||||
|
<span v-else-if="run.commit.branch.name" class="action-run-summary-branch-label tw-max-w-full">
|
||||||
|
<a
|
||||||
|
v-if="!run.commit.branch.isDeleted && run.commit.branch.link"
|
||||||
|
class="gt-ellipsis silenced"
|
||||||
|
:href="run.commit.branch.link"
|
||||||
|
:title="run.commit.branch.name"
|
||||||
|
>{{ run.commit.branch.name }}</a>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="gt-ellipsis tw-line-through"
|
||||||
|
:title="run.commit.branch.name"
|
||||||
|
>{{ run.commit.branch.name }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-text-block">
|
</div>
|
||||||
<ActionStatusIcon :locale-status="locale.status[run.status]" :status="run.status" :size="16" icon-variant="circle-fill"/>
|
|
||||||
<span>{{ locale.status[run.status] }}</span> • <span>{{ locale.totalDuration }} {{ run.duration || '–' }}</span>
|
<div class="action-run-summary-stat-divider"/>
|
||||||
|
|
||||||
|
<div class="action-run-summary-stat">
|
||||||
|
<span class="action-run-summary-label">{{ locale.statusLabel }}</span>
|
||||||
|
<span class="action-run-summary-stat-value">{{ locale.status[run.status] }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-run-summary-stat">
|
||||||
|
<span class="action-run-summary-label">{{ locale.totalDuration }}</span>
|
||||||
|
<span class="action-run-summary-stat-value">{{ run.duration || '–' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-run-summary-stat action-run-summary-stat-last">
|
||||||
|
<span class="action-run-summary-label">{{ locale.artifactsTitle }}</span>
|
||||||
|
<span class="action-run-summary-stat-value">{{ artifactsDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WorkflowGraph
|
<WorkflowGraph
|
||||||
@@ -63,6 +119,8 @@ onBeforeUnmount(() => {
|
|||||||
:jobs="topLevelJobs"
|
:jobs="topLevelJobs"
|
||||||
:run-link="run.link"
|
:run-link="run.link"
|
||||||
:workflow-id="run.workflowID"
|
:workflow-id="run.workflowID"
|
||||||
|
:workflow-link="`${run.link}/workflow`"
|
||||||
|
:trigger-event="run.triggerEvent"
|
||||||
:locale="locale"
|
:locale="locale"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,13 +135,119 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.action-run-summary-block {
|
.action-run-summary-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
align-items: stretch; /* equal-height columns so labels align at top and values at bottom */
|
||||||
padding: 12px;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid var(--color-secondary);
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
background: var(--color-console-bg);
|
||||||
background: var(--color-box-header);
|
}
|
||||||
|
|
||||||
|
.action-run-summary-trigger {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-trigger-content {
|
||||||
|
margin-top: auto; /* pin trigger content to the bottom, aligned with the stat values */
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-user {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-user .ui.avatar {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-pr {
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-branch-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 200px;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: var(--color-primary-light-6);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-family: var(--fonts-monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-branch-label a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-branch-label a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-user:hover span {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 72px;
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-stat-last {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-stat-divider {
|
||||||
|
display: none;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
margin: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-stat-value {
|
||||||
|
display: block;
|
||||||
|
margin-top: auto; /* pin value to the bottom so all column values share a baseline */
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.action-run-summary-trigger {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-stat {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-run-summary-stat-divider {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export function createEmptyActionsRun(): ActionsRun {
|
|||||||
duration: '',
|
duration: '',
|
||||||
triggeredAt: 0,
|
triggeredAt: 0,
|
||||||
triggerEvent: '',
|
triggerEvent: '',
|
||||||
|
pullRequest: null,
|
||||||
jobs: [] as Array<ActionsJob>,
|
jobs: [] as Array<ActionsJob>,
|
||||||
commit: {
|
commit: {
|
||||||
localeCommit: '',
|
localeCommit: '',
|
||||||
@@ -135,6 +136,7 @@ export function createEmptyActionsRun(): ActionsRun {
|
|||||||
pusher: {
|
pusher: {
|
||||||
displayName: '',
|
displayName: '',
|
||||||
link: '',
|
link: '',
|
||||||
|
avatarLink: '',
|
||||||
},
|
},
|
||||||
branch: {
|
branch: {
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ function formatCurrentAttemptTitle(attempt: ActionsRunAttempt) {
|
|||||||
return attempt.latest ? `${locale.latest} #${attempt.attempt}` : formatAttemptTitle(attempt);
|
return attempt.latest ? `${locale.latest} #${attempt.attempt}` : formatAttemptTitle(attempt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backLink = computed(() => {
|
||||||
|
if (run.value.pullRequest) {
|
||||||
|
return {href: run.value.pullRequest.link, prefix: locale.backToPullRequest, name: run.value.pullRequest.index};
|
||||||
|
}
|
||||||
|
if (run.value.workflowLink) {
|
||||||
|
return {href: run.value.workflowLink, prefix: locale.backToWorkflow, name: run.value.workflowID.replace(/\.(yml|yaml)$/i, '')};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
function buildArtifactLink(name: string) {
|
function buildArtifactLink(name: string) {
|
||||||
const searchString = run.value.runAttempt > 0 ? `?attempt=${run.value.runAttempt}` : '';
|
const searchString = run.value.runAttempt > 0 ? `?attempt=${run.value.runAttempt}` : '';
|
||||||
return `${run.value.link}/artifacts/${encodeURIComponent(name)}${searchString}`;
|
return `${run.value.link}/artifacts/${encodeURIComponent(name)}${searchString}`;
|
||||||
@@ -108,9 +118,13 @@ async function deleteArtifact(name: string) {
|
|||||||
<!-- make the view container full width to make users easier to read logs -->
|
<!-- make the view container full width to make users easier to read logs -->
|
||||||
<div class="ui fluid container">
|
<div class="ui fluid container">
|
||||||
<div class="action-view-header">
|
<div class="action-view-header">
|
||||||
|
<a v-if="backLink" class="action-view-back silenced" :href="backLink.href">
|
||||||
|
<SvgIcon name="octicon-arrow-left" :size="14"/>
|
||||||
|
<span>{{ backLink.prefix }} <span class="action-view-back-name">{{ backLink.name }}</span></span>
|
||||||
|
</a>
|
||||||
<div class="action-info-summary">
|
<div class="action-info-summary">
|
||||||
<div class="action-info-summary-title">
|
<div class="action-info-summary-title">
|
||||||
<ActionStatusIcon :locale-status="locale.status[run.status]" :status="run.status" :size="20" icon-variant="circle-fill"/>
|
<ActionStatusIcon :locale-status="locale.status[run.status]" :status="run.status" :size="22" icon-variant="circle-fill"/>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<h2 class="action-info-summary-title-text" v-html="run.titleHTML"/>
|
<h2 class="action-info-summary-title-text" v-html="run.titleHTML"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,26 +186,6 @@ async function deleteArtifact(name: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-commit-summary">
|
|
||||||
<span>
|
|
||||||
<a v-if="run.workflowLink" class="muted" :href="run.workflowLink"><b>{{ run.workflowID }}</b></a>
|
|
||||||
<b v-else>{{ run.workflowID }}</b>
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<template v-if="run.isSchedule">
|
|
||||||
{{ locale.scheduled }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ locale.commit }}
|
|
||||||
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
|
|
||||||
{{ locale.pushedBy }}
|
|
||||||
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
|
|
||||||
</template>
|
|
||||||
<span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
|
|
||||||
<span v-if="run.commit.branch.isDeleted" class="gt-ellipsis tw-line-through" :data-tooltip-content="run.commit.branch.name">{{ run.commit.branch.name }}</span>
|
|
||||||
<a v-else class="gt-ellipsis" :href="run.commit.branch.link" :data-tooltip-content="run.commit.branch.name">{{ run.commit.branch.name }}</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="action-view-body">
|
<div class="action-view-body">
|
||||||
<div class="action-view-left">
|
<div class="action-view-left">
|
||||||
@@ -287,6 +281,7 @@ async function deleteArtifact(name: string) {
|
|||||||
v-if="!props.jobId"
|
v-if="!props.jobId"
|
||||||
:store="store"
|
:store="store"
|
||||||
:locale="locale"
|
:locale="locale"
|
||||||
|
:artifact-count="artifacts.length"
|
||||||
/>
|
/>
|
||||||
<ActionRunJobView
|
<ActionRunJobView
|
||||||
v-else
|
v-else
|
||||||
@@ -311,9 +306,30 @@ async function deleteArtifact(name: string) {
|
|||||||
/* action view header */
|
/* action view header */
|
||||||
|
|
||||||
.action-view-header {
|
.action-view-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-view-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-light-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-view-back:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-view-back-name {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
.action-info-summary {
|
.action-info-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -340,21 +356,6 @@ async function deleteArtifact(name: string) {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-commit-summary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 5px;
|
|
||||||
margin-left: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.action-commit-summary {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================ */
|
/* ================ */
|
||||||
/* action view left */
|
/* action view left */
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const props = defineProps<{
|
|||||||
jobs: ActionsJob[];
|
jobs: ActionsJob[];
|
||||||
runLink: string;
|
runLink: string;
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
workflowLink?: string;
|
||||||
|
triggerEvent?: string;
|
||||||
locale: Record<string, string>;
|
locale: Record<string, string>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -231,9 +233,13 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="jobs.length > 0" class="workflow-graph">
|
<div v-if="jobs.length > 0" class="workflow-graph">
|
||||||
<div class="graph-header">
|
<div class="graph-header">
|
||||||
<h4 class="graph-title">{{ locale.workflowDependencies }}</h4>
|
<div class="graph-workflow-info">
|
||||||
|
<a v-if="workflowLink" class="graph-workflow-name silenced" :href="workflowLink">{{ workflowId }}</a>
|
||||||
|
<span v-else class="graph-workflow-name">{{ workflowId }}</span>
|
||||||
|
<div v-if="triggerEvent" class="graph-workflow-trigger">on: {{ triggerEvent }}</div>
|
||||||
|
</div>
|
||||||
<div class="graph-stats">{{ graphStats }}</div>
|
<div class="graph-stats">{{ graphStats }}</div>
|
||||||
<div class="flex-text-block">
|
<div class="flex-text-block graph-controls">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="zoomIn"
|
@click="zoomIn"
|
||||||
@@ -424,20 +430,29 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 14px;
|
padding: 16px 16px 8px;
|
||||||
background: var(--color-box-header);
|
background: var(--color-console-bg);
|
||||||
border-bottom: 1px solid var(--color-secondary);
|
|
||||||
gap: var(--gap-block);
|
gap: var(--gap-block);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-title {
|
.graph-workflow-info {
|
||||||
margin: 0;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-workflow-name {
|
||||||
|
display: block;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
flex: 1;
|
line-height: 1.25;
|
||||||
min-width: 200px;
|
}
|
||||||
|
|
||||||
|
.graph-workflow-trigger {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-stats {
|
.graph-stats {
|
||||||
@@ -447,6 +462,12 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
|||||||
color: var(--color-text-light-1);
|
color: var(--color-text-light-1);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-container {
|
.graph-container {
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export function initRepositoryActionView() {
|
|||||||
expandCallerJobs: el.getAttribute('data-locale-expand-caller-jobs'),
|
expandCallerJobs: el.getAttribute('data-locale-expand-caller-jobs'),
|
||||||
collapseCallerJobs: el.getAttribute('data-locale-collapse-caller-jobs'),
|
collapseCallerJobs: el.getAttribute('data-locale-collapse-caller-jobs'),
|
||||||
triggeredVia: el.getAttribute('data-locale-triggered-via'),
|
triggeredVia: el.getAttribute('data-locale-triggered-via'),
|
||||||
|
rerunTriggered: el.getAttribute('data-locale-rerun-triggered'),
|
||||||
|
backToPullRequest: el.getAttribute('data-locale-back-to-pull-request'),
|
||||||
|
backToWorkflow: el.getAttribute('data-locale-back-to-workflow'),
|
||||||
|
statusLabel: el.getAttribute('data-locale-status-label'),
|
||||||
totalDuration: el.getAttribute('data-locale-total-duration'),
|
totalDuration: el.getAttribute('data-locale-total-duration'),
|
||||||
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
|
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
|
||||||
artifactExpired: el.getAttribute('data-locale-artifact-expired'),
|
artifactExpired: el.getAttribute('data-locale-artifact-expired'),
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export type ActionsRun = {
|
|||||||
duration: string,
|
duration: string,
|
||||||
triggeredAt: number,
|
triggeredAt: number,
|
||||||
triggerEvent: string,
|
triggerEvent: string,
|
||||||
|
pullRequest?: {
|
||||||
|
index: string,
|
||||||
|
link: string,
|
||||||
|
} | null,
|
||||||
jobs: Array<ActionsJob>,
|
jobs: Array<ActionsJob>,
|
||||||
commit: {
|
commit: {
|
||||||
localeCommit: string,
|
localeCommit: string,
|
||||||
@@ -32,6 +36,7 @@ export type ActionsRun = {
|
|||||||
pusher: {
|
pusher: {
|
||||||
displayName: string,
|
displayName: string,
|
||||||
link: string,
|
link: string,
|
||||||
|
avatarLink: string,
|
||||||
},
|
},
|
||||||
branch: {
|
branch: {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -51,6 +56,7 @@ export type ActionsRunAttempt = {
|
|||||||
triggeredAt: number;
|
triggeredAt: number;
|
||||||
triggerUserName: string;
|
triggerUserName: string;
|
||||||
triggerUserLink: string;
|
triggerUserLink: string;
|
||||||
|
triggerUserAvatar: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionsJob = {
|
export type ActionsJob = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox
|
|||||||
import giteaExclamation from '../../public/assets/img/svg/gitea-exclamation.svg';
|
import giteaExclamation from '../../public/assets/img/svg/gitea-exclamation.svg';
|
||||||
import giteaRunning from '../../public/assets/img/svg/gitea-running.svg';
|
import giteaRunning from '../../public/assets/img/svg/gitea-running.svg';
|
||||||
import octiconArchive from '../../public/assets/img/svg/octicon-archive.svg';
|
import octiconArchive from '../../public/assets/img/svg/octicon-archive.svg';
|
||||||
|
import octiconArrowLeft from '../../public/assets/img/svg/octicon-arrow-left.svg';
|
||||||
import octiconArrowSwitch from '../../public/assets/img/svg/octicon-arrow-switch.svg';
|
import octiconArrowSwitch from '../../public/assets/img/svg/octicon-arrow-switch.svg';
|
||||||
import octiconBlocked from '../../public/assets/img/svg/octicon-blocked.svg';
|
import octiconBlocked from '../../public/assets/img/svg/octicon-blocked.svg';
|
||||||
import octiconBold from '../../public/assets/img/svg/octicon-bold.svg';
|
import octiconBold from '../../public/assets/img/svg/octicon-bold.svg';
|
||||||
@@ -94,6 +95,7 @@ const svgs = {
|
|||||||
'gitea-exclamation': giteaExclamation,
|
'gitea-exclamation': giteaExclamation,
|
||||||
'gitea-running': giteaRunning,
|
'gitea-running': giteaRunning,
|
||||||
'octicon-archive': octiconArchive,
|
'octicon-archive': octiconArchive,
|
||||||
|
'octicon-arrow-left': octiconArrowLeft,
|
||||||
'octicon-arrow-switch': octiconArrowSwitch,
|
'octicon-arrow-switch': octiconArrowSwitch,
|
||||||
'octicon-blocked': octiconBlocked,
|
'octicon-blocked': octiconBlocked,
|
||||||
'octicon-bold': octiconBold,
|
'octicon-bold': octiconBold,
|
||||||
|
|||||||
Reference in New Issue
Block a user