diff --git a/appview/db/pipeline.go b/appview/db/pipeline.go index e845f608..9c76806f 100644 --- a/appview/db/pipeline.go +++ b/appview/db/pipeline.go @@ -7,6 +7,7 @@ import ( "time" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing" spindle "tangled.sh/tangled.sh/core/spindle/models" ) @@ -60,6 +61,23 @@ func (p Pipeline) Counts() map[string]int { return m } +func (p Pipeline) TimeTaken() time.Duration { + var s time.Duration + for _, w := range p.Statuses { + s += w.TimeTaken() + } + return s +} + +func (p Pipeline) Workflows() []string { + var ws []string + for v := range p.Statuses { + ws = append(ws, v) + } + slices.Sort(ws) + return ws +} + type Trigger struct { Id int Kind string @@ -76,6 +94,24 @@ type Trigger struct { PRAction *string } +func (t *Trigger) IsPush() bool { + return t != nil && t.Kind == "push" +} + +func (t *Trigger) IsPullRequest() bool { + return t != nil && t.Kind == "pull_request" +} + +func (t *Trigger) TargetRef() string { + if t.IsPush() { + return plumbing.ReferenceName(*t.PushRef).Short() + } else if t.IsPullRequest() { + return *t.PRTargetBranch + } + + return "" +} + type PipelineStatus struct { ID int Spindle string @@ -262,14 +298,14 @@ func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) { query := fmt.Sprintf(` select - p.id AS pipeline_id, + p.id, p.knot, p.rkey, p.repo_owner, p.repo_name, p.sha, p.created, - t.id AS trigger_id, + t.id, t.kind, t.push_ref, t.push_new_sha, @@ -283,7 +319,6 @@ func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) { join triggers t ON p.trigger_id = t.id %s - order by p.created desc `, whereClause) rows, err := e.Query(query, args...) @@ -425,5 +460,13 @@ func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) { all = append(all, p) } + // sort pipelines by date + slices.SortFunc(all, func(a, b Pipeline) int { + if a.Created.After(b.Created) { + return -1 + } + return 1 + }) + return all, nil } diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index 0005fa85..3a294905 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -225,6 +225,13 @@ func funcMap() template.FuncMap { } return dict, nil }, + "deref": func(v any) any { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr && !val.IsNil() { + return val.Elem().Interface() + } + return nil + }, "i": func(name string, classes ...string) template.HTML { data, err := icon(name, classes) if err != nil { diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 23e3318d..16912af3 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -951,6 +951,18 @@ func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff}) } +type PipelinesParams struct { + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Pipelines []db.Pipeline + Active string +} + +func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { + params.Active = "pipelines" + return p.executeRepo("repo/pipelines/pipelines", w, params) +} + func (p *Pages) Static() http.Handler { if p.dev { return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go index 8b1ca8c8..0c9dcb31 100644 --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -40,6 +40,7 @@ func (r RepoInfo) GetTabs() [][]string { {"overview", "/", "square-chart-gantt"}, {"issues", "/issues", "circle-dot"}, {"pulls", "/pulls", "git-pull-request"}, + {"pipelines", "/pipelines", "layers-2"}, } if r.Roles.SettingsAllowed() { diff --git a/appview/pages/templates/repo/commit.html b/appview/pages/templates/repo/commit.html index 702320ed..98b46399 100644 --- a/appview/pages/templates/repo/commit.html +++ b/appview/pages/templates/repo/commit.html @@ -71,7 +71,7 @@
{{ if $.Pipeline }} - {{ template "repo/fragments/pipelineStatusSymbol" $.Pipeline }} + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $.Pipeline }} {{ end }}
diff --git a/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html b/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html deleted file mode 100644 index 235c6351..00000000 --- a/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html +++ /dev/null @@ -1,119 +0,0 @@ -{{ define "repo/fragments/pipelineStatusSymbol" }} -
- {{ block "icon" $ }} {{ end }} - {{ block "tooltip" $ }} {{ end }} -
-{{ end }} - -{{ define "icon" }} -
- {{ $c := .Counts }} - {{ $statuses := .Statuses }} - {{ $total := len $statuses }} - {{ $success := index $c "success" }} - {{ $allPass := eq $success $total }} - - {{ if $allPass }} -
- {{ i "check" "size-4 text-green-600 dark:text-green-400 " }} - {{ $total }}/{{ $total }} -
- {{ else }} - {{ $radius := f64 8 }} - {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} - {{ $offset := 0.0 }} -
- - - - {{ range $kind, $count := $c }} - {{ $color := "" }} - {{ if or (eq $kind "pending") (eq $kind "running") }} - {{ $color = "#eab308" }} - {{ else if eq $kind "success" }} - {{ $color = "#10b981" }} - {{ else if eq $kind "cancelled" }} - {{ $color = "#6b7280" }} - {{ else }} - {{ $color = "#ef4444" }} - {{ end }} - - {{ $percent := divf64 (f64 $count) (f64 $total) }} - {{ $length := mulf64 $percent $circumference }} - - - {{ $offset = addf64 $offset $length }} - {{ end }} - - {{$success}}/{{ $total }} -
- {{ end }} -
-{{ end }} - -{{ define "tooltip" }} - -{{ end }} diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html index 3db6d571..87740333 100644 --- a/appview/pages/templates/repo/index.html +++ b/appview/pages/templates/repo/index.html @@ -275,7 +275,7 @@ {{ $pipeline := index $.Pipelines .Hash.String }} {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
- {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }} {{ end }} diff --git a/appview/pages/templates/repo/log.html b/appview/pages/templates/repo/log.html index 1f9994d7..dc7c5f1c 100644 --- a/appview/pages/templates/repo/log.html +++ b/appview/pages/templates/repo/log.html @@ -84,7 +84,7 @@ {{ $pipeline := index $.Pipelines .Hash.String }} {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} - {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }} {{ end }} {{ timeFmt $commit.Committer.When }} @@ -170,7 +170,7 @@ {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
- {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }} {{ end }} diff --git a/appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html new file mode 100644 index 00000000..fab91ddc --- /dev/null +++ b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html @@ -0,0 +1,53 @@ +{{ define "repo/pipelines/fragments/pipelineSymbol" }} +
+ {{ $c := .Counts }} + {{ $statuses := .Statuses }} + {{ $total := len $statuses }} + {{ $success := index $c "success" }} + {{ $allPass := eq $success $total }} + + {{ if $allPass }} +
+ {{ i "check" "size-4 text-green-600 dark:text-green-400 " }} + {{ $total }}/{{ $total }} +
+ {{ else }} + {{ $radius := f64 8 }} + {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} + {{ $offset := 0.0 }} +
+ + + + {{ range $kind, $count := $c }} + {{ $color := "" }} + {{ if or (eq $kind "pending") (eq $kind "running") }} + {{ $color = "#eab308" }} + {{ else if eq $kind "success" }} + {{ $color = "#10b981" }} + {{ else if eq $kind "cancelled" }} + {{ $color = "#6b7280" }} + {{ else }} + {{ $color = "#ef4444" }} + {{ end }} + + {{ $percent := divf64 (f64 $count) (f64 $total) }} + {{ $length := mulf64 $percent $circumference }} + + + {{ $offset = addf64 $offset $length }} + {{ end }} + + {{ $success }}/{{ $total }} +
+ {{ end }} +
+{{ end }} + diff --git a/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html new file mode 100644 index 00000000..3acc4d2e --- /dev/null +++ b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html @@ -0,0 +1,8 @@ +{{ define "repo/pipelines/fragments/pipelineSymbolLong" }} +
+ {{ template "repo/pipelines/fragments/pipelineSymbol" $ }} + {{ template "repo/pipelines/fragments/tooltip" $ }} +
+{{ end }} + + diff --git a/appview/pages/templates/repo/pipelines/fragments/tooltip.html b/appview/pages/templates/repo/pipelines/fragments/tooltip.html new file mode 100644 index 00000000..c313f87f --- /dev/null +++ b/appview/pages/templates/repo/pipelines/fragments/tooltip.html @@ -0,0 +1,30 @@ +{{ define "repo/pipelines/fragments/tooltip" }} + +{{ end }} + diff --git a/appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html b/appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html new file mode 100644 index 00000000..2b2b4d1e --- /dev/null +++ b/appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html @@ -0,0 +1,26 @@ +{{ define "repo/pipelines/fragments/workflowSymbol" }} + {{ $lastStatus := .Latest }} + {{ $kind := $lastStatus.Status.String }} + + {{ $icon := "dot" }} + {{ $color := "text-gray-600 dark:text-gray-500" }} + + {{ if eq $kind "pending" }} + {{ $icon = "circle-dashed" }} + {{ $color = "text-yellow-600 dark:text-yellow-500" }} + {{ else if eq $kind "running" }} + {{ $icon = "circle-dashed" }} + {{ $color = "text-yellow-600 dark:text-yellow-500" }} + {{ else if eq $kind "success" }} + {{ $icon = "check" }} + {{ $color = "text-green-600 dark:text-green-500" }} + {{ else if eq $kind "cancelled" }} + {{ $icon = "circle-slash" }} + {{ $color = "text-gray-600 dark:text-gray-500" }} + {{ else }} + {{ $icon = "x" }} + {{ $color = "text-red-600 dark:text-red-500" }} + {{ end }} + + {{ i $icon "size-4" $color }} +{{ end }} diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html new file mode 100644 index 00000000..69811420 --- /dev/null +++ b/appview/pages/templates/repo/pipelines/pipelines.html @@ -0,0 +1,93 @@ +{{ define "title" }}pipelines · {{ .RepoInfo.FullName }}{{ end }} + +{{ define "extrameta" }} + {{ $title := "pipelines"}} + {{ $url := printf "https://tangled.sh/%s/pipelines" .RepoInfo.FullName }} + {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} +{{ end }} + +{{ define "repoContent" }} +
+
+
+ +
+
+{{ end }} + +{{ define "repoAfter" }} +
+ {{ range .Pipelines }} + {{ block "pipeline" (list $ .) }} {{ end }} + {{ else }} +

+ No pipelines run for this repository. +

+ {{ end }} +
+{{ end }} + +{{ define "pipeline" }} + {{ $root := index . 0 }} + {{ $p := index . 1 }} +
+ {{ block "pipelineHeader" $ }} {{ end }} +
+{{ end }} + +{{ define "pipelineHeader" }} + {{ $root := index . 0 }} + {{ $p := index . 1 }} + {{ with $p }} +
+
+ {{ $target := .Trigger.TargetRef }} + {{ $workflows := .Workflows }} + {{ if .Trigger.IsPush }} + + {{ $target }} + push + + + {{ else if .Trigger.IsPullRequest }} + + pull request + + {{ $target }} + {{ i "arrow-left" "size-4" }} + {{ .Trigger.PRSourceBranch }} + + + {{ end }} +
+ +
+ {{ template "repo/pipelines/fragments/pipelineSymbolLong" . }} +
+ +
+ +
+ + {{ $t := .TimeTaken }} +
+ {{ if $t }} + + {{ else }} + + {{ end }} +
+
+ {{ end }} +{{ end }} diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go index 4eeff88b..95794509 100644 --- a/appview/pipelines/pipelines.go +++ b/appview/pipelines/pipelines.go @@ -56,3 +56,33 @@ func New( Logger: logger, } } + +func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) { + user := p.oauth.GetUser(r) + l := p.Logger.With("handler", "Index") + + f, err := p.repoResolver.Resolve(r) + if err != nil { + l.Error("failed to get repo and knot", "err", err) + return + } + + repoInfo := f.RepoInfo(user) + + ps, err := db.GetPipelineStatuses( + p.db, + db.FilterEq("repo_owner", repoInfo.OwnerDid), + db.FilterEq("repo_name", repoInfo.Name), + db.FilterEq("knot", repoInfo.Knot), + ) + if err != nil { + l.Error("failed to query db", "err", err) + return + } + + p.pages.Pipelines(w, pages.PipelinesParams{ + LoggedInUser: user, + RepoInfo: repoInfo, + Pipelines: ps, + }) +}