forked from tangled.org/core
Monorepo for Tangled — https://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
···
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
+
"github.com/go-git/go-git/v5/plumbing"
spindle "tangled.sh/tangled.sh/core/spindle/models"
)
···
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
···
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
···
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,
···
join
triggers t ON p.trigger_id = t.id
%s
-
order by p.created desc
`, whereClause)
rows, err := e.Query(query, args...)
···
}
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
}
+7
appview/pages/funcmap.go
···
}
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 {
+12
appview/pages/pages.go
···
return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.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")))
+1
appview/pages/repoinfo/repoinfo.go
···
{"overview", "/", "square-chart-gantt"},
{"issues", "/issues", "circle-dot"},
{"pulls", "/pulls", "git-pull-request"},
+
{"pipelines", "/pipelines", "layers-2"},
}
if r.Roles.SettingsAllowed() {
+1 -1
appview/pages/templates/repo/commit.html
···
<div class="text-sm">
{{ if $.Pipeline }}
-
{{ template "repo/fragments/pipelineStatusSymbol" $.Pipeline }}
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" $.Pipeline }}
{{ end }}
</div>
</div>
-119
appview/pages/templates/repo/fragments/pipelineStatusSymbol.html
···
-
{{ define "repo/fragments/pipelineStatusSymbol" }}
-
<div class="group relative inline-block">
-
{{ block "icon" $ }} {{ end }}
-
{{ block "tooltip" $ }} {{ end }}
-
</div>
-
{{ end }}
-
-
{{ define "icon" }}
-
<div class="cursor-pointer">
-
{{ $c := .Counts }}
-
{{ $statuses := .Statuses }}
-
{{ $total := len $statuses }}
-
{{ $success := index $c "success" }}
-
{{ $allPass := eq $success $total }}
-
-
{{ if $allPass }}
-
<div class="flex gap-1 items-center">
-
{{ i "check" "size-4 text-green-600 dark:text-green-400 " }}
-
<span>{{ $total }}/{{ $total }}</span>
-
</div>
-
{{ else }}
-
{{ $radius := f64 8 }}
-
{{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }}
-
{{ $offset := 0.0 }}
-
<div class="flex gap-1 items-center">
-
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20">
-
<circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/>
-
-
{{ 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 }}
-
-
<circle
-
cx="10" cy="10" r="{{ $radius }}"
-
fill="none"
-
stroke="{{ $color }}"
-
stroke-width="2"
-
stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}"
-
stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}"
-
/>
-
{{ $offset = addf64 $offset $length }}
-
{{ end }}
-
</svg>
-
<span>{{$success}}/{{ $total }}</span>
-
</div>
-
{{ end }}
-
</div>
-
{{ end }}
-
-
{{ define "tooltip" }}
-
<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">
-
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700">
-
{{ range $name, $all := .Statuses }}
-
<div class="flex items-center justify-between p-1">
-
{{ $lastStatus := $all.Latest }}
-
{{ $kind := $lastStatus.Status.String }}
-
-
{{ $icon := "dot" }}
-
{{ $color := "text-gray-600 dark:text-gray-500" }}
-
{{ $text := "Failed" }}
-
{{ $time := "" }}
-
-
{{ if eq $kind "pending" }}
-
{{ $icon = "circle-dashed" }}
-
{{ $color = "text-yellow-600 dark:text-yellow-500" }}
-
{{ $text = "Queued" }}
-
{{ $time = timeFmt $lastStatus.Created }}
-
{{ else if eq $kind "running" }}
-
{{ $icon = "circle-dashed" }}
-
{{ $color = "text-yellow-600 dark:text-yellow-500" }}
-
{{ $text = "Running" }}
-
{{ $time = timeFmt $lastStatus.Created }}
-
{{ else if eq $kind "success" }}
-
{{ $icon = "check" }}
-
{{ $color = "text-green-600 dark:text-green-500" }}
-
{{ $text = "Success" }}
-
{{ with $all.TimeTaken }}
-
{{ $time = durationFmt . }}
-
{{ end }}
-
{{ else if eq $kind "cancelled" }}
-
{{ $icon = "circle-slash" }}
-
{{ $color = "text-gray-600 dark:text-gray-500" }}
-
{{ $text = "Cancelled" }}
-
{{ with $all.TimeTaken }}
-
{{ $time = durationFmt . }}
-
{{ end }}
-
{{ else }}
-
{{ $icon = "x" }}
-
{{ $color = "text-red-600 dark:text-red-500" }}
-
{{ $text = "Failed" }}
-
{{ with $all.TimeTaken }}
-
{{ $time = durationFmt . }}
-
{{ end }}
-
{{ end }}
-
-
<div id="left" class="flex items-center gap-2 flex-shrink-0">
-
{{ i $icon "size-4" $color }}
-
{{ $name }}
-
</div>
-
<div id="right" class="flex items-center gap-2 flex-shrink-0">
-
<span class="font-bold">{{ $text }}</span>
-
<time class="text-gray-400 dark:text-gray-600">{{ $time }}</time>
-
</div>
-
</div>
-
{{ end }}
-
</div>
-
</div>
-
{{ end }}
+1 -1
appview/pages/templates/repo/index.html
···
{{ $pipeline := index $.Pipelines .Hash.String }}
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
<div class="inline-block px-1 select-none after:content-['·']"></div>
-
{{ template "repo/fragments/pipelineStatusSymbol" $pipeline }}
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }}
{{ end }}
</div>
</div>
+2 -2
appview/pages/templates/repo/log.html
···
<!-- ci status -->
{{ $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 }}
</td>
<td class=" py-3 align-top text-gray-500 dark:text-gray-400">{{ timeFmt $commit.Committer.When }}</td>
···
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
<div class="inline-block px-1 select-none after:content-['·']"></div>
<span class="text-sm">
-
{{ template "repo/fragments/pipelineStatusSymbol" $pipeline }}
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }}
</span>
{{ end }}
</div>
+53
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
···
+
{{ define "repo/pipelines/fragments/pipelineSymbol" }}
+
<div class="cursor-pointer">
+
{{ $c := .Counts }}
+
{{ $statuses := .Statuses }}
+
{{ $total := len $statuses }}
+
{{ $success := index $c "success" }}
+
{{ $allPass := eq $success $total }}
+
+
{{ if $allPass }}
+
<div class="flex gap-1 items-center">
+
{{ i "check" "size-4 text-green-600 dark:text-green-400 " }}
+
<span>{{ $total }}/{{ $total }}</span>
+
</div>
+
{{ else }}
+
{{ $radius := f64 8 }}
+
{{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }}
+
{{ $offset := 0.0 }}
+
<div class="flex gap-1 items-center">
+
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20">
+
<circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/>
+
+
{{ 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 }}
+
+
<circle
+
cx="10" cy="10" r="{{ $radius }}"
+
fill="none"
+
stroke="{{ $color }}"
+
stroke-width="2"
+
stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}"
+
stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}"
+
/>
+
{{ $offset = addf64 $offset $length }}
+
{{ end }}
+
</svg>
+
<span>{{ $success }}/{{ $total }}</span>
+
</div>
+
{{ end }}
+
</div>
+
{{ end }}
+
+8
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
···
+
{{ define "repo/pipelines/fragments/pipelineSymbolLong" }}
+
<div class="group relative inline-block">
+
{{ template "repo/pipelines/fragments/pipelineSymbol" $ }}
+
{{ template "repo/pipelines/fragments/tooltip" $ }}
+
</div>
+
{{ end }}
+
+
+30
appview/pages/templates/repo/pipelines/fragments/tooltip.html
···
+
{{ define "repo/pipelines/fragments/tooltip" }}
+
<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">
+
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700">
+
{{ range $name, $all := .Statuses }}
+
<div class="flex items-center justify-between p-2">
+
{{ $lastStatus := $all.Latest }}
+
{{ $kind := $lastStatus.Status.String }}
+
+
{{ $t := .TimeTaken }}
+
{{ $time := "" }}
+
{{ if $t }}
+
{{ $time = durationFmt $t }}
+
{{ else }}
+
{{ $time = printf "%s ago" (shortTimeFmt $.Created) }}
+
{{ end }}
+
+
<div id="left" class="flex items-center gap-2 flex-shrink-0">
+
{{ template "repo/pipelines/fragments/workflowSymbol" $all }}
+
{{ $name }}
+
</div>
+
<div id="right" class="flex items-center gap-2 flex-shrink-0">
+
<span class="font-bold">{{ $kind }}</span>
+
<time>{{ $time }}</time>
+
</div>
+
</div>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+
+26
appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html
···
+
{{ 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 }}
+93
appview/pages/templates/repo/pipelines/pipelines.html
···
+
{{ define "title" }}pipelines &middot; {{ .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" }}
+
<div class="flex justify-between items-center gap-4">
+
<div class="flex gap-4">
+
</div>
+
+
</div>
+
<div class="error" id="issues"></div>
+
{{ end }}
+
+
{{ define "repoAfter" }}
+
<section
+
class="w-full flex flex-col gap-2 mt-2"
+
>
+
{{ range .Pipelines }}
+
{{ block "pipeline" (list $ .) }} {{ end }}
+
{{ else }}
+
<p class="text-center pt-5 text-gray-400 dark:text-gray-500">
+
No pipelines run for this repository.
+
</p>
+
{{ end }}
+
</section>
+
{{ end }}
+
+
{{ define "pipeline" }}
+
{{ $root := index . 0 }}
+
{{ $p := index . 1 }}
+
<div class="py-4 px-6 bg-white dark:bg-gray-800 dark:text-white">
+
{{ block "pipelineHeader" $ }} {{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "pipelineHeader" }}
+
{{ $root := index . 0 }}
+
{{ $p := index . 1 }}
+
{{ with $p }}
+
<div class="grid grid-cols-4 md:grid-cols-8 gap-2 items-center w-full">
+
<div class="col-span-1 md:col-span-5 flex items-center gap-4">
+
{{ $target := .Trigger.TargetRef }}
+
{{ $workflows := .Workflows }}
+
{{ if .Trigger.IsPush }}
+
<a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ .Id }}/workflow/{{ index $workflows 0 }}" class="block">
+
<span class="font-bold">{{ $target }}</span>
+
<span>push</span>
+
</a>
+
<span class="hidden md:inline-flex gap-2 items-center font-mono text-sm">
+
{{ $old := deref .Trigger.PushOldSha }}
+
{{ $new := deref .Trigger.PushNewSha }}
+
+
<a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $new }}">{{ slice $new 0 8 }}</a>
+
{{ i "arrow-left" "size-4" }}
+
<a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $old }}">{{ slice $old 0 8 }}</a>
+
</span>
+
{{ else if .Trigger.IsPullRequest }}
+
<span>
+
pull request
+
<span class="inline-flex gap-2 items-center">
+
{{ $target }}
+
{{ i "arrow-left" "size-4" }}
+
{{ .Trigger.PRSourceBranch }}
+
</span>
+
</span>
+
{{ end }}
+
</div>
+
+
<div class="col-span-1 pl-4">
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" . }}
+
</div>
+
+
<div class="col-span-1 text-right">
+
<time title="{{ .Created | longTimeFmt }}">
+
{{ .Created | shortTimeFmt }} ago
+
</time>
+
</div>
+
+
{{ $t := .TimeTaken }}
+
<div class="col-span-1 text-right">
+
{{ if $t }}
+
<time title="{{ $t }}">{{ $t | durationFmt }}</time>
+
{{ else }}
+
<time>--</time>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+
{{ end }}
+30
appview/pipelines/pipelines.go
···
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,
+
})
+
}