Monorepo for Tangled tangled.org

appview: add index page for pipelines tab

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 372731c8 6bf1f194

verified
+46 -3
appview/db/pipeline.go
··· 7 7 "time" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/go-git/go-git/v5/plumbing" 10 11 spindle "tangled.sh/tangled.sh/core/spindle/models" 11 12 ) 12 13 ··· 60 61 return m 61 62 } 62 63 64 + func (p Pipeline) TimeTaken() time.Duration { 65 + var s time.Duration 66 + for _, w := range p.Statuses { 67 + s += w.TimeTaken() 68 + } 69 + return s 70 + } 71 + 72 + func (p Pipeline) Workflows() []string { 73 + var ws []string 74 + for v := range p.Statuses { 75 + ws = append(ws, v) 76 + } 77 + slices.Sort(ws) 78 + return ws 79 + } 80 + 63 81 type Trigger struct { 64 82 Id int 65 83 Kind string ··· 76 94 PRAction *string 77 95 } 78 96 97 + func (t *Trigger) IsPush() bool { 98 + return t != nil && t.Kind == "push" 99 + } 100 + 101 + func (t *Trigger) IsPullRequest() bool { 102 + return t != nil && t.Kind == "pull_request" 103 + } 104 + 105 + func (t *Trigger) TargetRef() string { 106 + if t.IsPush() { 107 + return plumbing.ReferenceName(*t.PushRef).Short() 108 + } else if t.IsPullRequest() { 109 + return *t.PRTargetBranch 110 + } 111 + 112 + return "" 113 + } 114 + 79 115 type PipelineStatus struct { 80 116 ID int 81 117 Spindle string ··· 262 298 263 299 query := fmt.Sprintf(` 264 300 select 265 - p.id AS pipeline_id, 301 + p.id, 266 302 p.knot, 267 303 p.rkey, 268 304 p.repo_owner, 269 305 p.repo_name, 270 306 p.sha, 271 307 p.created, 272 - t.id AS trigger_id, 308 + t.id, 273 309 t.kind, 274 310 t.push_ref, 275 311 t.push_new_sha, ··· 283 319 join 284 320 triggers t ON p.trigger_id = t.id 285 321 %s 286 - order by p.created desc 287 322 `, whereClause) 288 323 289 324 rows, err := e.Query(query, args...) ··· 424 459 } 425 460 all = append(all, p) 426 461 } 462 + 463 + // sort pipelines by date 464 + slices.SortFunc(all, func(a, b Pipeline) int { 465 + if a.Created.After(b.Created) { 466 + return -1 467 + } 468 + return 1 469 + }) 427 470 428 471 return all, nil 429 472 }
+7
appview/pages/funcmap.go
··· 225 225 } 226 226 return dict, nil 227 227 }, 228 + "deref": func(v any) any { 229 + val := reflect.ValueOf(v) 230 + if val.Kind() == reflect.Ptr && !val.IsNil() { 231 + return val.Elem().Interface() 232 + } 233 + return nil 234 + }, 228 235 "i": func(name string, classes ...string) template.HTML { 229 236 data, err := icon(name, classes) 230 237 if err != nil {
+12
appview/pages/pages.go
··· 951 951 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 952 952 } 953 953 954 + type PipelinesParams struct { 955 + LoggedInUser *oauth.User 956 + RepoInfo repoinfo.RepoInfo 957 + Pipelines []db.Pipeline 958 + Active string 959 + } 960 + 961 + func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 962 + params.Active = "pipelines" 963 + return p.executeRepo("repo/pipelines/pipelines", w, params) 964 + } 965 + 954 966 func (p *Pages) Static() http.Handler { 955 967 if p.dev { 956 968 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
+1
appview/pages/repoinfo/repoinfo.go
··· 40 40 {"overview", "/", "square-chart-gantt"}, 41 41 {"issues", "/issues", "circle-dot"}, 42 42 {"pulls", "/pulls", "git-pull-request"}, 43 + {"pipelines", "/pipelines", "layers-2"}, 43 44 } 44 45 45 46 if r.Roles.SettingsAllowed() {
+1 -1
appview/pages/templates/repo/commit.html
··· 71 71 72 72 <div class="text-sm"> 73 73 {{ if $.Pipeline }} 74 - {{ template "repo/fragments/pipelineStatusSymbol" $.Pipeline }} 74 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $.Pipeline }} 75 75 {{ end }} 76 76 </div> 77 77 </div>
-119
appview/pages/templates/repo/fragments/pipelineStatusSymbol.html
··· 1 - {{ define "repo/fragments/pipelineStatusSymbol" }} 2 - <div class="group relative inline-block"> 3 - {{ block "icon" $ }} {{ end }} 4 - {{ block "tooltip" $ }} {{ end }} 5 - </div> 6 - {{ end }} 7 - 8 - {{ define "icon" }} 9 - <div class="cursor-pointer"> 10 - {{ $c := .Counts }} 11 - {{ $statuses := .Statuses }} 12 - {{ $total := len $statuses }} 13 - {{ $success := index $c "success" }} 14 - {{ $allPass := eq $success $total }} 15 - 16 - {{ if $allPass }} 17 - <div class="flex gap-1 items-center"> 18 - {{ i "check" "size-4 text-green-600 dark:text-green-400 " }} 19 - <span>{{ $total }}/{{ $total }}</span> 20 - </div> 21 - {{ else }} 22 - {{ $radius := f64 8 }} 23 - {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} 24 - {{ $offset := 0.0 }} 25 - <div class="flex gap-1 items-center"> 26 - <svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20"> 27 - <circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/> 28 - 29 - {{ range $kind, $count := $c }} 30 - {{ $color := "" }} 31 - {{ if or (eq $kind "pending") (eq $kind "running") }} 32 - {{ $color = "#eab308" }} 33 - {{ else if eq $kind "success" }} 34 - {{ $color = "#10b981" }} 35 - {{ else if eq $kind "cancelled" }} 36 - {{ $color = "#6b7280" }} 37 - {{ else }} 38 - {{ $color = "#ef4444" }} 39 - {{ end }} 40 - 41 - {{ $percent := divf64 (f64 $count) (f64 $total) }} 42 - {{ $length := mulf64 $percent $circumference }} 43 - 44 - <circle 45 - cx="10" cy="10" r="{{ $radius }}" 46 - fill="none" 47 - stroke="{{ $color }}" 48 - stroke-width="2" 49 - stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}" 50 - stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}" 51 - /> 52 - {{ $offset = addf64 $offset $length }} 53 - {{ end }} 54 - </svg> 55 - <span>{{$success}}/{{ $total }}</span> 56 - </div> 57 - {{ end }} 58 - </div> 59 - {{ end }} 60 - 61 - {{ define "tooltip" }} 62 - <div class="absolute z-[9999] hidden group-hover:block bg-white dark:bg-gray-900 text-sm text-black dark:text-white rounded-md shadow p-2 w-80 top-full mt-2"> 63 - <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700"> 64 - {{ range $name, $all := .Statuses }} 65 - <div class="flex items-center justify-between p-1"> 66 - {{ $lastStatus := $all.Latest }} 67 - {{ $kind := $lastStatus.Status.String }} 68 - 69 - {{ $icon := "dot" }} 70 - {{ $color := "text-gray-600 dark:text-gray-500" }} 71 - {{ $text := "Failed" }} 72 - {{ $time := "" }} 73 - 74 - {{ if eq $kind "pending" }} 75 - {{ $icon = "circle-dashed" }} 76 - {{ $color = "text-yellow-600 dark:text-yellow-500" }} 77 - {{ $text = "Queued" }} 78 - {{ $time = timeFmt $lastStatus.Created }} 79 - {{ else if eq $kind "running" }} 80 - {{ $icon = "circle-dashed" }} 81 - {{ $color = "text-yellow-600 dark:text-yellow-500" }} 82 - {{ $text = "Running" }} 83 - {{ $time = timeFmt $lastStatus.Created }} 84 - {{ else if eq $kind "success" }} 85 - {{ $icon = "check" }} 86 - {{ $color = "text-green-600 dark:text-green-500" }} 87 - {{ $text = "Success" }} 88 - {{ with $all.TimeTaken }} 89 - {{ $time = durationFmt . }} 90 - {{ end }} 91 - {{ else if eq $kind "cancelled" }} 92 - {{ $icon = "circle-slash" }} 93 - {{ $color = "text-gray-600 dark:text-gray-500" }} 94 - {{ $text = "Cancelled" }} 95 - {{ with $all.TimeTaken }} 96 - {{ $time = durationFmt . }} 97 - {{ end }} 98 - {{ else }} 99 - {{ $icon = "x" }} 100 - {{ $color = "text-red-600 dark:text-red-500" }} 101 - {{ $text = "Failed" }} 102 - {{ with $all.TimeTaken }} 103 - {{ $time = durationFmt . }} 104 - {{ end }} 105 - {{ end }} 106 - 107 - <div id="left" class="flex items-center gap-2 flex-shrink-0"> 108 - {{ i $icon "size-4" $color }} 109 - {{ $name }} 110 - </div> 111 - <div id="right" class="flex items-center gap-2 flex-shrink-0"> 112 - <span class="font-bold">{{ $text }}</span> 113 - <time class="text-gray-400 dark:text-gray-600">{{ $time }}</time> 114 - </div> 115 - </div> 116 - {{ end }} 117 - </div> 118 - </div> 119 - {{ end }}
+1 -1
appview/pages/templates/repo/index.html
··· 275 275 {{ $pipeline := index $.Pipelines .Hash.String }} 276 276 {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} 277 277 <div class="inline-block px-1 select-none after:content-['·']"></div> 278 - {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} 278 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }} 279 279 {{ end }} 280 280 </div> 281 281 </div>
+2 -2
appview/pages/templates/repo/log.html
··· 84 84 <!-- ci status --> 85 85 {{ $pipeline := index $.Pipelines .Hash.String }} 86 86 {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} 87 - {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} 87 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }} 88 88 {{ end }} 89 89 </td> 90 90 <td class=" py-3 align-top text-gray-500 dark:text-gray-400">{{ timeFmt $commit.Committer.When }}</td> ··· 170 170 {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} 171 171 <div class="inline-block px-1 select-none after:content-['·']"></div> 172 172 <span class="text-sm"> 173 - {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} 173 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }} 174 174 </span> 175 175 {{ end }} 176 176 </div>
+53
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
··· 1 + {{ define "repo/pipelines/fragments/pipelineSymbol" }} 2 + <div class="cursor-pointer"> 3 + {{ $c := .Counts }} 4 + {{ $statuses := .Statuses }} 5 + {{ $total := len $statuses }} 6 + {{ $success := index $c "success" }} 7 + {{ $allPass := eq $success $total }} 8 + 9 + {{ if $allPass }} 10 + <div class="flex gap-1 items-center"> 11 + {{ i "check" "size-4 text-green-600 dark:text-green-400 " }} 12 + <span>{{ $total }}/{{ $total }}</span> 13 + </div> 14 + {{ else }} 15 + {{ $radius := f64 8 }} 16 + {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} 17 + {{ $offset := 0.0 }} 18 + <div class="flex gap-1 items-center"> 19 + <svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20"> 20 + <circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/> 21 + 22 + {{ range $kind, $count := $c }} 23 + {{ $color := "" }} 24 + {{ if or (eq $kind "pending") (eq $kind "running") }} 25 + {{ $color = "#eab308" }} 26 + {{ else if eq $kind "success" }} 27 + {{ $color = "#10b981" }} 28 + {{ else if eq $kind "cancelled" }} 29 + {{ $color = "#6b7280" }} 30 + {{ else }} 31 + {{ $color = "#ef4444" }} 32 + {{ end }} 33 + 34 + {{ $percent := divf64 (f64 $count) (f64 $total) }} 35 + {{ $length := mulf64 $percent $circumference }} 36 + 37 + <circle 38 + cx="10" cy="10" r="{{ $radius }}" 39 + fill="none" 40 + stroke="{{ $color }}" 41 + stroke-width="2" 42 + stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}" 43 + stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}" 44 + /> 45 + {{ $offset = addf64 $offset $length }} 46 + {{ end }} 47 + </svg> 48 + <span>{{ $success }}/{{ $total }}</span> 49 + </div> 50 + {{ end }} 51 + </div> 52 + {{ end }} 53 +
+8
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
··· 1 + {{ define "repo/pipelines/fragments/pipelineSymbolLong" }} 2 + <div class="group relative inline-block"> 3 + {{ template "repo/pipelines/fragments/pipelineSymbol" $ }} 4 + {{ template "repo/pipelines/fragments/tooltip" $ }} 5 + </div> 6 + {{ end }} 7 + 8 +
+30
appview/pages/templates/repo/pipelines/fragments/tooltip.html
··· 1 + {{ define "repo/pipelines/fragments/tooltip" }} 2 + <div class="absolute z-[9999] hidden group-hover:block bg-white dark:bg-gray-900 text-black dark:text-white rounded-md shadow w-80 top-full mt-2"> 3 + <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700"> 4 + {{ range $name, $all := .Statuses }} 5 + <div class="flex items-center justify-between p-2"> 6 + {{ $lastStatus := $all.Latest }} 7 + {{ $kind := $lastStatus.Status.String }} 8 + 9 + {{ $t := .TimeTaken }} 10 + {{ $time := "" }} 11 + {{ if $t }} 12 + {{ $time = durationFmt $t }} 13 + {{ else }} 14 + {{ $time = printf "%s ago" (shortTimeFmt $.Created) }} 15 + {{ end }} 16 + 17 + <div id="left" class="flex items-center gap-2 flex-shrink-0"> 18 + {{ template "repo/pipelines/fragments/workflowSymbol" $all }} 19 + {{ $name }} 20 + </div> 21 + <div id="right" class="flex items-center gap-2 flex-shrink-0"> 22 + <span class="font-bold">{{ $kind }}</span> 23 + <time>{{ $time }}</time> 24 + </div> 25 + </div> 26 + {{ end }} 27 + </div> 28 + </div> 29 + {{ end }} 30 +
+26
appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html
··· 1 + {{ define "repo/pipelines/fragments/workflowSymbol" }} 2 + {{ $lastStatus := .Latest }} 3 + {{ $kind := $lastStatus.Status.String }} 4 + 5 + {{ $icon := "dot" }} 6 + {{ $color := "text-gray-600 dark:text-gray-500" }} 7 + 8 + {{ if eq $kind "pending" }} 9 + {{ $icon = "circle-dashed" }} 10 + {{ $color = "text-yellow-600 dark:text-yellow-500" }} 11 + {{ else if eq $kind "running" }} 12 + {{ $icon = "circle-dashed" }} 13 + {{ $color = "text-yellow-600 dark:text-yellow-500" }} 14 + {{ else if eq $kind "success" }} 15 + {{ $icon = "check" }} 16 + {{ $color = "text-green-600 dark:text-green-500" }} 17 + {{ else if eq $kind "cancelled" }} 18 + {{ $icon = "circle-slash" }} 19 + {{ $color = "text-gray-600 dark:text-gray-500" }} 20 + {{ else }} 21 + {{ $icon = "x" }} 22 + {{ $color = "text-red-600 dark:text-red-500" }} 23 + {{ end }} 24 + 25 + {{ i $icon "size-4" $color }} 26 + {{ end }}
+93
appview/pages/templates/repo/pipelines/pipelines.html
··· 1 + {{ define "title" }}pipelines &middot; {{ .RepoInfo.FullName }}{{ end }} 2 + 3 + {{ define "extrameta" }} 4 + {{ $title := "pipelines"}} 5 + {{ $url := printf "https://tangled.sh/%s/pipelines" .RepoInfo.FullName }} 6 + {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 7 + {{ end }} 8 + 9 + {{ define "repoContent" }} 10 + <div class="flex justify-between items-center gap-4"> 11 + <div class="flex gap-4"> 12 + </div> 13 + 14 + </div> 15 + <div class="error" id="issues"></div> 16 + {{ end }} 17 + 18 + {{ define "repoAfter" }} 19 + <section 20 + class="w-full flex flex-col gap-2 mt-2" 21 + > 22 + {{ range .Pipelines }} 23 + {{ block "pipeline" (list $ .) }} {{ end }} 24 + {{ else }} 25 + <p class="text-center pt-5 text-gray-400 dark:text-gray-500"> 26 + No pipelines run for this repository. 27 + </p> 28 + {{ end }} 29 + </section> 30 + {{ end }} 31 + 32 + {{ define "pipeline" }} 33 + {{ $root := index . 0 }} 34 + {{ $p := index . 1 }} 35 + <div class="py-4 px-6 bg-white dark:bg-gray-800 dark:text-white"> 36 + {{ block "pipelineHeader" $ }} {{ end }} 37 + </div> 38 + {{ end }} 39 + 40 + {{ define "pipelineHeader" }} 41 + {{ $root := index . 0 }} 42 + {{ $p := index . 1 }} 43 + {{ with $p }} 44 + <div class="grid grid-cols-4 md:grid-cols-8 gap-2 items-center w-full"> 45 + <div class="col-span-1 md:col-span-5 flex items-center gap-4"> 46 + {{ $target := .Trigger.TargetRef }} 47 + {{ $workflows := .Workflows }} 48 + {{ if .Trigger.IsPush }} 49 + <a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ .Id }}/workflow/{{ index $workflows 0 }}" class="block"> 50 + <span class="font-bold">{{ $target }}</span> 51 + <span>push</span> 52 + </a> 53 + <span class="hidden md:inline-flex gap-2 items-center font-mono text-sm"> 54 + {{ $old := deref .Trigger.PushOldSha }} 55 + {{ $new := deref .Trigger.PushNewSha }} 56 + 57 + <a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $new }}">{{ slice $new 0 8 }}</a> 58 + {{ i "arrow-left" "size-4" }} 59 + <a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $old }}">{{ slice $old 0 8 }}</a> 60 + </span> 61 + {{ else if .Trigger.IsPullRequest }} 62 + <span> 63 + pull request 64 + <span class="inline-flex gap-2 items-center"> 65 + {{ $target }} 66 + {{ i "arrow-left" "size-4" }} 67 + {{ .Trigger.PRSourceBranch }} 68 + </span> 69 + </span> 70 + {{ end }} 71 + </div> 72 + 73 + <div class="col-span-1 pl-4"> 74 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" . }} 75 + </div> 76 + 77 + <div class="col-span-1 text-right"> 78 + <time title="{{ .Created | longTimeFmt }}"> 79 + {{ .Created | shortTimeFmt }} ago 80 + </time> 81 + </div> 82 + 83 + {{ $t := .TimeTaken }} 84 + <div class="col-span-1 text-right"> 85 + {{ if $t }} 86 + <time title="{{ $t }}">{{ $t | durationFmt }}</time> 87 + {{ else }} 88 + <time>--</time> 89 + {{ end }} 90 + </div> 91 + </div> 92 + {{ end }} 93 + {{ end }}
+30
appview/pipelines/pipelines.go
··· 56 56 Logger: logger, 57 57 } 58 58 } 59 + 60 + func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) { 61 + user := p.oauth.GetUser(r) 62 + l := p.Logger.With("handler", "Index") 63 + 64 + f, err := p.repoResolver.Resolve(r) 65 + if err != nil { 66 + l.Error("failed to get repo and knot", "err", err) 67 + return 68 + } 69 + 70 + repoInfo := f.RepoInfo(user) 71 + 72 + ps, err := db.GetPipelineStatuses( 73 + p.db, 74 + db.FilterEq("repo_owner", repoInfo.OwnerDid), 75 + db.FilterEq("repo_name", repoInfo.Name), 76 + db.FilterEq("knot", repoInfo.Knot), 77 + ) 78 + if err != nil { 79 + l.Error("failed to query db", "err", err) 80 + return 81 + } 82 + 83 + p.pages.Pipelines(w, pages.PipelinesParams{ 84 + LoggedInUser: user, 85 + RepoInfo: repoInfo, 86 + Pipelines: ps, 87 + }) 88 + }