+47
appview/pages/funcmap.go
+47
appview/pages/funcmap.go
···
49
49
"sub": func(a, b int) int {
50
50
return a - b
51
51
},
52
+
"f64": func(a int) float64 {
53
+
return float64(a)
54
+
},
55
+
"addf64": func(a, b float64) float64 {
56
+
return a + b
57
+
},
58
+
"subf64": func(a, b float64) float64 {
59
+
return a - b
60
+
},
61
+
"mulf64": func(a, b float64) float64 {
62
+
return a * b
63
+
},
64
+
"divf64": func(a, b float64) float64 {
65
+
if b == 0 {
66
+
return 0
67
+
}
68
+
return a / b
69
+
},
70
+
"negf64": func(a float64) float64 {
71
+
return -a
72
+
},
52
73
"cond": func(cond interface{}, a, b string) string {
53
74
if cond == nil {
54
75
return b
···
104
125
{humanize.LongTime, "%dy %s", humanize.Year},
105
126
{math.MaxInt64, "a long while %s", 1},
106
127
})
128
+
},
129
+
"durationFmt": func(duration time.Duration) string {
130
+
days := int64(duration.Hours() / 24)
131
+
hours := int64(math.Mod(duration.Hours(), 24))
132
+
minutes := int64(math.Mod(duration.Minutes(), 60))
133
+
seconds := int64(math.Mod(duration.Seconds(), 60))
134
+
135
+
chunks := []struct {
136
+
name string
137
+
amount int64
138
+
}{
139
+
{"d", days},
140
+
{"hr", hours},
141
+
{"min", minutes},
142
+
{"s", seconds},
143
+
}
144
+
145
+
parts := []string{}
146
+
147
+
for _, chunk := range chunks {
148
+
if chunk.amount != 0 {
149
+
parts = append(parts, fmt.Sprintf("%d%s", chunk.amount, chunk.name))
150
+
}
151
+
}
152
+
153
+
return strings.Join(parts, " ")
107
154
},
108
155
"byteFmt": humanize.Bytes,
109
156
"length": func(slice any) int {
+1
appview/pages/pages.go
+1
appview/pages/pages.go
+118
appview/pages/templates/repo/fragments/pipelineStatusSymbol.html
+118
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 " }} {{ $total }}/{{ $total }}
19
+
</div>
20
+
{{ else }}
21
+
{{ $radius := f64 8 }}
22
+
{{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }}
23
+
{{ $offset := 0.0 }}
24
+
<div class="flex gap-1 items-center">
25
+
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20">
26
+
<circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/>
27
+
28
+
{{ range $kind, $count := $c }}
29
+
{{ $color := "" }}
30
+
{{ if or (eq $kind "pending") (eq $kind "running") }}
31
+
{{ $color = "#eab308" }}
32
+
{{ else if eq $kind "success" }}
33
+
{{ $color = "#10b981" }}
34
+
{{ else if eq $kind "cancelled" }}
35
+
{{ $color = "#6b7280" }}
36
+
{{ else }}
37
+
{{ $color = "#ef4444" }}
38
+
{{ end }}
39
+
40
+
{{ $percent := divf64 (f64 $count) (f64 $total) }}
41
+
{{ $length := mulf64 $percent $circumference }}
42
+
43
+
<circle
44
+
cx="10" cy="10" r="{{ $radius }}"
45
+
fill="none"
46
+
stroke="{{ $color }}"
47
+
stroke-width="2"
48
+
stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}"
49
+
stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}"
50
+
/>
51
+
{{ $offset = addf64 $offset $length }}
52
+
{{ end }}
53
+
</svg>
54
+
<span>{{$success}}/{{ $total }}</span>
55
+
</div>
56
+
{{ end }}
57
+
</div>
58
+
{{ end }}
59
+
60
+
{{ define "tooltip" }}
61
+
<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">
62
+
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700">
63
+
{{ range $name, $all := .Statuses }}
64
+
<div class="flex items-center justify-between p-1">
65
+
{{ $lastStatus := $all.Latest }}
66
+
{{ $kind := $lastStatus.Status.String }}
67
+
68
+
{{ $icon := "dot" }}
69
+
{{ $color := "text-gray-600 dark:text-gray-500" }}
70
+
{{ $text := "Failed" }}
71
+
{{ $time := "" }}
72
+
73
+
{{ if eq $kind "pending" }}
74
+
{{ $icon = "circle-dashed" }}
75
+
{{ $color = "text-yellow-600 dark:text-yellow-500" }}
76
+
{{ $text = "Queued" }}
77
+
{{ $time = timeFmt $lastStatus.Created }}
78
+
{{ else if eq $kind "running" }}
79
+
{{ $icon = "circle-dashed" }}
80
+
{{ $color = "text-yellow-600 dark:text-yellow-500" }}
81
+
{{ $text = "Running" }}
82
+
{{ $time = timeFmt $lastStatus.Created }}
83
+
{{ else if eq $kind "success" }}
84
+
{{ $icon = "check" }}
85
+
{{ $color = "text-green-600 dark:text-green-500" }}
86
+
{{ $text = "Success" }}
87
+
{{ with $all.TimeTaken }}
88
+
{{ $time = durationFmt . }}
89
+
{{ end }}
90
+
{{ else if eq $kind "cancelled" }}
91
+
{{ $icon = "circle-slash" }}
92
+
{{ $color = "text-gray-600 dark:text-gray-500" }}
93
+
{{ $text = "Cancelled" }}
94
+
{{ with $all.TimeTaken }}
95
+
{{ $time = durationFmt . }}
96
+
{{ end }}
97
+
{{ else }}
98
+
{{ $icon = "x" }}
99
+
{{ $color = "text-red-600 dark:text-red-500" }}
100
+
{{ $text = "Failed" }}
101
+
{{ with $all.TimeTaken }}
102
+
{{ $time = durationFmt . }}
103
+
{{ end }}
104
+
{{ end }}
105
+
106
+
<div id="left" class="flex items-center gap-2 flex-shrink-0">
107
+
{{ i $icon "size-4" $color }}
108
+
{{ $name }}
109
+
</div>
110
+
<div id="right" class="flex items-center gap-2 flex-shrink-0">
111
+
<span class="font-bold">{{ $text }}</span>
112
+
<time class="text-gray-400 dark:text-gray-600">{{ $time }}</time>
113
+
</div>
114
+
</div>
115
+
{{ end }}
116
+
</div>
117
+
</div>
118
+
{{ end }}
+22
-18
appview/pages/templates/repo/index.html
+22
-18
appview/pages/templates/repo/index.html
···
222
222
</div>
223
223
</div>
224
224
225
+
<!-- commit info bar -->
225
226
<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center">
226
227
{{ $verified := $.VerifiedCommits.IsVerified .Hash.String }}
227
228
{{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }}
···
238
239
</a>
239
240
</span>
240
241
<span
241
-
class="mx-2 before:content-['·'] before:select-none"
242
+
class="mx-1 before:content-['·'] before:select-none"
242
243
></span>
243
244
<span>
244
245
{{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }}
···
256
257
{{ end }}</a
257
258
>
258
259
</span>
259
-
<div
260
-
class="inline-block px-1 select-none after:content-['·']"
261
-
></div>
262
-
<span>{{ timeFmt .Committer.When }}</span>
263
-
{{ $tagsForCommit := index $.TagMap .Hash.String }}
264
-
{{ if gt (len $tagsForCommit) 0 }}
265
-
<div
266
-
class="inline-block px-1 select-none after:content-['·']"
267
-
></div>
268
-
{{ end }}
269
-
{{ range $tagsForCommit }}
270
-
<span
271
-
class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"
272
-
>
273
-
{{ . }}
274
-
</span>
275
-
{{ end }}
260
+
<div class="inline-block px-1 select-none after:content-['·']"></div>
261
+
<span>{{ timeFmt .Committer.When }}</span>
262
+
263
+
<!-- tags/branches -->
264
+
{{ $tagsForCommit := index $.TagMap .Hash.String }}
265
+
{{ if gt (len $tagsForCommit) 0 }}
266
+
<div class="inline-block px-1 select-none after:content-['·']"></div>
267
+
{{ end }}
268
+
{{ range $tagsForCommit }}
269
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-[2px] inline-flex items-center">
270
+
{{ . }}
271
+
</span>
272
+
{{ end }}
273
+
274
+
<!-- ci status -->
275
+
{{ $pipeline := index $.Pipelines .Hash }}
276
+
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
277
+
<div class="inline-block px-1 select-none after:content-['·']"></div>
278
+
{{ template "repo/fragments/pipelineStatusSymbol" $pipeline }}
279
+
{{ end }}
276
280
</div>
277
281
</div>
278
282
{{ end }}
+7
appview/repo/index.go
+7
appview/repo/index.go
···
127
127
// non-fatal
128
128
}
129
129
130
+
pipelines, err := rp.getPipelineStatuses(repoInfo, commitsTrunc)
131
+
if err != nil {
132
+
log.Printf("failed to fetch pipeline statuses: %s", err)
133
+
// non-fatal
134
+
}
135
+
130
136
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
131
137
LoggedInUser: user,
132
138
RepoInfo: repoInfo,
···
139
145
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
140
146
VerifiedCommits: vc,
141
147
Languages: repoLanguages,
148
+
Pipelines: pipelines,
142
149
})
143
150
return
144
151
}
+41
appview/repo/repo_util.go
+41
appview/repo/repo_util.go
···
6
6
"fmt"
7
7
"math/big"
8
8
9
+
"tangled.sh/tangled.sh/core/appview/db"
10
+
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
11
+
12
+
"github.com/go-git/go-git/v5/plumbing"
9
13
"github.com/go-git/go-git/v5/plumbing/object"
10
14
)
11
15
···
98
102
99
103
return string(result)
100
104
}
105
+
106
+
// grab pipelines from DB and munge that into a hashmap with commit sha as key
107
+
//
108
+
// golang is so blessed that it requires 35 lines of imperative code for this
109
+
func (rp *Repo) getPipelineStatuses(
110
+
repoInfo repoinfo.RepoInfo,
111
+
commits []*object.Commit,
112
+
) (map[plumbing.Hash]db.Pipeline, error) {
113
+
m := make(map[plumbing.Hash]db.Pipeline)
114
+
115
+
if len(commits) == 0 {
116
+
return m, nil
117
+
}
118
+
119
+
shas := make([]string, len(commits))
120
+
for _, c := range commits {
121
+
shas = append(shas, c.Hash.String())
122
+
}
123
+
124
+
ps, err := db.GetPipelineStatuses(
125
+
rp.db,
126
+
db.FilterEq("repo_owner", repoInfo.OwnerDid),
127
+
db.FilterEq("repo_name", repoInfo.Name),
128
+
db.FilterEq("knot", repoInfo.Knot),
129
+
db.FilterIn("sha", shas),
130
+
)
131
+
if err != nil {
132
+
return nil, err
133
+
}
134
+
135
+
for _, p := range ps {
136
+
hash := plumbing.NewHash(p.Sha)
137
+
m[hash] = p
138
+
}
139
+
140
+
return m, nil
141
+
}
+11
-3
appview/state/spindlestream.go
+11
-3
appview/state/spindlestream.go
···
4
4
"context"
5
5
"encoding/json"
6
6
"log/slog"
7
+
"strings"
7
8
"time"
8
9
9
10
"github.com/bluesky-social/indigo/atproto/syntax"
···
15
16
"tangled.sh/tangled.sh/core/eventconsumer/cursor"
16
17
"tangled.sh/tangled.sh/core/log"
17
18
"tangled.sh/tangled.sh/core/rbac"
19
+
spindle "tangled.sh/tangled.sh/core/spindle/models"
18
20
)
19
21
20
22
func Spindlestream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer) (*ec.Consumer, error) {
···
77
79
exitCode = int(*record.ExitCode)
78
80
}
79
81
82
+
// pick the record creation time if possible, or use time.Now
83
+
created := time.Now()
84
+
if t, err := time.Parse(time.RFC3339, record.CreatedAt); err == nil && created.After(t) {
85
+
created = t
86
+
}
87
+
80
88
status := db.PipelineStatus{
81
89
Spindle: source.Key(),
82
90
Rkey: msg.Rkey,
83
-
PipelineKnot: pipelineUri.Authority().String(),
91
+
PipelineKnot: strings.TrimPrefix(pipelineUri.Authority().String(), "did:web:"),
84
92
PipelineRkey: pipelineUri.RecordKey().String(),
85
-
Created: time.Now(),
93
+
Created: created,
86
94
Workflow: record.Workflow,
87
-
Status: record.Status,
95
+
Status: spindle.StatusKind(record.Status),
88
96
Error: record.Error,
89
97
ExitCode: exitCode,
90
98
}