Merge 453499267f41ae8b3e4bd97a7445ec1c9de27425 into 70c00ad2d5a23676a0759d7ae2dcab5e5dcd4425
8 files · +213 -45
M internal/adapters/web/actions.templ +3 -0
| @@ -456,6 +456,9 @@ templ MRChecks(c ChecksSection) { | |||
| 456 | 456 | } else { | |
| 457 | 457 | <ul class="checks"> | |
| 458 | 458 | for _, r := range c.Rows { | |
| 459 | + | if r.GroupLabel != "" { | |
| 460 | + | <li class="ck-group"><a href={ templ.SafeURL(r.GroupHref) }>{ r.GroupLabel }</a></li> | |
| 461 | + | } | |
| 459 | 462 | <li> | |
| 460 | 463 | @CheckIcon(r.Kind, r.State) | |
| 461 | 464 | if r.Href != "" { | |
| @@ -456,6 +456,9 @@ templ MRChecks(c ChecksSection) { | |||||
| 456 | } else { | 456 | } else { | ||
| 457 | <ul class="checks"> | 457 | <ul class="checks"> | ||
| 458 | for _, r := range c.Rows { | 458 | for _, r := range c.Rows { | ||
| 459 | + | if r.GroupLabel != "" { | |||
| 460 | + | <li class="ck-group"><a href={ templ.SafeURL(r.GroupHref) }>{ r.GroupLabel }</a></li> | |||
| 461 | + | } | |||
| 459 | <li> | 462 | <li> | ||
| 460 | @CheckIcon(r.Kind, r.State) | 463 | @CheckIcon(r.Kind, r.State) | ||
| 461 | if r.Href != "" { | 464 | if r.Href != "" { | ||
M internal/adapters/web/actions_templ.go +66 -34
| @@ -3146,7 +3146,39 @@ func MRChecks(c ChecksSection) templ.Component { | |||
| 3146 | 3146 | return templ_7745c5c3_Err | |
| 3147 | 3147 | } | |
| 3148 | 3148 | for _, r := range c.Rows { | |
| 3149 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li>") | |
| 3149 | + | if r.GroupLabel != "" { | |
| 3150 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li class=\"ck-group\"><a href=\"") | |
| 3151 | + | if templ_7745c5c3_Err != nil { | |
| 3152 | + | return templ_7745c5c3_Err | |
| 3153 | + | } | |
| 3154 | + | var templ_7745c5c3_Var165 templ.SafeURL | |
| 3155 | + | templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.GroupHref)) | |
| 3156 | + | if templ_7745c5c3_Err != nil { | |
| 3157 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 63} | |
| 3158 | + | } | |
| 3159 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165)) | |
| 3160 | + | if templ_7745c5c3_Err != nil { | |
| 3161 | + | return templ_7745c5c3_Err | |
| 3162 | + | } | |
| 3163 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\">") | |
| 3164 | + | if templ_7745c5c3_Err != nil { | |
| 3165 | + | return templ_7745c5c3_Err | |
| 3166 | + | } | |
| 3167 | + | var templ_7745c5c3_Var166 string | |
| 3168 | + | templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.GroupLabel) | |
| 3169 | + | if templ_7745c5c3_Err != nil { | |
| 3170 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 80} | |
| 3171 | + | } | |
| 3172 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166)) | |
| 3173 | + | if templ_7745c5c3_Err != nil { | |
| 3174 | + | return templ_7745c5c3_Err | |
| 3175 | + | } | |
| 3176 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</a></li>") | |
| 3177 | + | if templ_7745c5c3_Err != nil { | |
| 3178 | + | return templ_7745c5c3_Err | |
| 3179 | + | } | |
| 3180 | + | } | |
| 3181 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, " <li>") | |
| 3150 | 3182 | if templ_7745c5c3_Err != nil { | |
| 3151 | 3183 | return templ_7745c5c3_Err | |
| 3152 | 3184 | } | |
| @@ -3155,108 +3187,108 @@ func MRChecks(c ChecksSection) templ.Component { | |||
| 3155 | 3187 | return templ_7745c5c3_Err | |
| 3156 | 3188 | } | |
| 3157 | 3189 | if r.Href != "" { | |
| 3158 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<a href=\"") | |
| 3190 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<a href=\"") | |
| 3159 | 3191 | if templ_7745c5c3_Err != nil { | |
| 3160 | 3192 | return templ_7745c5c3_Err | |
| 3161 | 3193 | } | |
| 3162 | - | var templ_7745c5c3_Var165 templ.SafeURL | |
| 3163 | - | templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 3194 | + | var templ_7745c5c3_Var167 templ.SafeURL | |
| 3195 | + | templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 3164 | 3196 | if templ_7745c5c3_Err != nil { | |
| 3165 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 38} | |
| 3197 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 38} | |
| 3166 | 3198 | } | |
| 3167 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165)) | |
| 3199 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167)) | |
| 3168 | 3200 | if templ_7745c5c3_Err != nil { | |
| 3169 | 3201 | return templ_7745c5c3_Err | |
| 3170 | 3202 | } | |
| 3171 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">") | |
| 3203 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "\">") | |
| 3172 | 3204 | if templ_7745c5c3_Err != nil { | |
| 3173 | 3205 | return templ_7745c5c3_Err | |
| 3174 | 3206 | } | |
| 3175 | - | var templ_7745c5c3_Var166 string | |
| 3176 | - | templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 3207 | + | var templ_7745c5c3_Var168 string | |
| 3208 | + | templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 3177 | 3209 | if templ_7745c5c3_Err != nil { | |
| 3178 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 49} | |
| 3210 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 49} | |
| 3179 | 3211 | } | |
| 3180 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166)) | |
| 3212 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168)) | |
| 3181 | 3213 | if templ_7745c5c3_Err != nil { | |
| 3182 | 3214 | return templ_7745c5c3_Err | |
| 3183 | 3215 | } | |
| 3184 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</a> ") | |
| 3216 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</a> ") | |
| 3185 | 3217 | if templ_7745c5c3_Err != nil { | |
| 3186 | 3218 | return templ_7745c5c3_Err | |
| 3187 | 3219 | } | |
| 3188 | 3220 | } else { | |
| 3189 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span>") | |
| 3221 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "<span>") | |
| 3190 | 3222 | if templ_7745c5c3_Err != nil { | |
| 3191 | 3223 | return templ_7745c5c3_Err | |
| 3192 | 3224 | } | |
| 3193 | - | var templ_7745c5c3_Var167 string | |
| 3194 | - | templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 3225 | + | var templ_7745c5c3_Var169 string | |
| 3226 | + | templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 3195 | 3227 | if templ_7745c5c3_Err != nil { | |
| 3196 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 464, Col: 21} | |
| 3228 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 467, Col: 21} | |
| 3197 | 3229 | } | |
| 3198 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167)) | |
| 3230 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169)) | |
| 3199 | 3231 | if templ_7745c5c3_Err != nil { | |
| 3200 | 3232 | return templ_7745c5c3_Err | |
| 3201 | 3233 | } | |
| 3202 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span> ") | |
| 3234 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</span> ") | |
| 3203 | 3235 | if templ_7745c5c3_Err != nil { | |
| 3204 | 3236 | return templ_7745c5c3_Err | |
| 3205 | 3237 | } | |
| 3206 | 3238 | } | |
| 3207 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<span class=\"dim\">") | |
| 3239 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "<span class=\"dim\">") | |
| 3208 | 3240 | if templ_7745c5c3_Err != nil { | |
| 3209 | 3241 | return templ_7745c5c3_Err | |
| 3210 | 3242 | } | |
| 3211 | - | var templ_7745c5c3_Var168 string | |
| 3212 | - | templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.State) | |
| 3243 | + | var templ_7745c5c3_Var170 string | |
| 3244 | + | templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(r.State) | |
| 3213 | 3245 | if templ_7745c5c3_Err != nil { | |
| 3214 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 466, Col: 33} | |
| 3246 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 469, Col: 33} | |
| 3215 | 3247 | } | |
| 3216 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168)) | |
| 3248 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170)) | |
| 3217 | 3249 | if templ_7745c5c3_Err != nil { | |
| 3218 | 3250 | return templ_7745c5c3_Err | |
| 3219 | 3251 | } | |
| 3220 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> ") | |
| 3252 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</span> ") | |
| 3221 | 3253 | if templ_7745c5c3_Err != nil { | |
| 3222 | 3254 | return templ_7745c5c3_Err | |
| 3223 | 3255 | } | |
| 3224 | 3256 | if r.Waiting { | |
| 3225 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<span class=\"mute\">waiting for a runner</span>") | |
| 3257 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<span class=\"mute\">waiting for a runner</span>") | |
| 3226 | 3258 | if templ_7745c5c3_Err != nil { | |
| 3227 | 3259 | return templ_7745c5c3_Err | |
| 3228 | 3260 | } | |
| 3229 | 3261 | } | |
| 3230 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</li>") | |
| 3262 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</li>") | |
| 3231 | 3263 | if templ_7745c5c3_Err != nil { | |
| 3232 | 3264 | return templ_7745c5c3_Err | |
| 3233 | 3265 | } | |
| 3234 | 3266 | } | |
| 3235 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>") | |
| 3267 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</ul>") | |
| 3236 | 3268 | if templ_7745c5c3_Err != nil { | |
| 3237 | 3269 | return templ_7745c5c3_Err | |
| 3238 | 3270 | } | |
| 3239 | 3271 | } | |
| 3240 | 3272 | if c.Note != "" { | |
| 3241 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"rule-note\"><span class=\"sr\">note: </span>") | |
| 3273 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<p class=\"rule-note\"><span class=\"sr\">note: </span>") | |
| 3242 | 3274 | if templ_7745c5c3_Err != nil { | |
| 3243 | 3275 | return templ_7745c5c3_Err | |
| 3244 | 3276 | } | |
| 3245 | - | var templ_7745c5c3_Var169 string | |
| 3246 | - | templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note) | |
| 3277 | + | var templ_7745c5c3_Var171 string | |
| 3278 | + | templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note) | |
| 3247 | 3279 | if templ_7745c5c3_Err != nil { | |
| 3248 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 475, Col: 62} | |
| 3280 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 478, Col: 62} | |
| 3249 | 3281 | } | |
| 3250 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169)) | |
| 3282 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171)) | |
| 3251 | 3283 | if templ_7745c5c3_Err != nil { | |
| 3252 | 3284 | return templ_7745c5c3_Err | |
| 3253 | 3285 | } | |
| 3254 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>") | |
| 3286 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "</p>") | |
| 3255 | 3287 | if templ_7745c5c3_Err != nil { | |
| 3256 | 3288 | return templ_7745c5c3_Err | |
| 3257 | 3289 | } | |
| 3258 | 3290 | } | |
| 3259 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</section>") | |
| 3291 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</section>") | |
| 3260 | 3292 | if templ_7745c5c3_Err != nil { | |
| 3261 | 3293 | return templ_7745c5c3_Err | |
| 3262 | 3294 | } | |
| @@ -3146,7 +3146,39 @@ func MRChecks(c ChecksSection) templ.Component { | |||||
| 3146 | return templ_7745c5c3_Err | 3146 | return templ_7745c5c3_Err | ||
| 3147 | } | 3147 | } | ||
| 3148 | for _, r := range c.Rows { | 3148 | for _, r := range c.Rows { | ||
| 3149 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li>") | 3149 | + | if r.GroupLabel != "" { |
| 3150 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li class=\"ck-group\"><a href=\"") | |||
| 3151 | + | if templ_7745c5c3_Err != nil { | |||
| 3152 | + | return templ_7745c5c3_Err | |||
| 3153 | + | } | |||
| 3154 | + | var templ_7745c5c3_Var165 templ.SafeURL | |||
| 3155 | + | templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.GroupHref)) | |||
| 3156 | + | if templ_7745c5c3_Err != nil { | |||
| 3157 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 63} | |||
| 3158 | + | } | |||
| 3159 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165)) | |||
| 3160 | + | if templ_7745c5c3_Err != nil { | |||
| 3161 | + | return templ_7745c5c3_Err | |||
| 3162 | + | } | |||
| 3163 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\">") | |||
| 3164 | + | if templ_7745c5c3_Err != nil { | |||
| 3165 | + | return templ_7745c5c3_Err | |||
| 3166 | + | } | |||
| 3167 | + | var templ_7745c5c3_Var166 string | |||
| 3168 | + | templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.GroupLabel) | |||
| 3169 | + | if templ_7745c5c3_Err != nil { | |||
| 3170 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 80} | |||
| 3171 | + | } | |||
| 3172 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166)) | |||
| 3173 | + | if templ_7745c5c3_Err != nil { | |||
| 3174 | + | return templ_7745c5c3_Err | |||
| 3175 | + | } | |||
| 3176 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</a></li>") | |||
| 3177 | + | if templ_7745c5c3_Err != nil { | |||
| 3178 | + | return templ_7745c5c3_Err | |||
| 3179 | + | } | |||
| 3180 | + | } | |||
| 3181 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, " <li>") | |||
| 3150 | if templ_7745c5c3_Err != nil { | 3182 | if templ_7745c5c3_Err != nil { | ||
| 3151 | return templ_7745c5c3_Err | 3183 | return templ_7745c5c3_Err | ||
| 3152 | } | 3184 | } | ||
| @@ -3155,108 +3187,108 @@ func MRChecks(c ChecksSection) templ.Component { | |||||
| 3155 | return templ_7745c5c3_Err | 3187 | return templ_7745c5c3_Err | ||
| 3156 | } | 3188 | } | ||
| 3157 | if r.Href != "" { | 3189 | if r.Href != "" { | ||
| 3158 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<a href=\"") | 3190 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<a href=\"") |
| 3159 | if templ_7745c5c3_Err != nil { | 3191 | if templ_7745c5c3_Err != nil { | ||
| 3160 | return templ_7745c5c3_Err | 3192 | return templ_7745c5c3_Err | ||
| 3161 | } | 3193 | } | ||
| 3162 | - | var templ_7745c5c3_Var165 templ.SafeURL | 3194 | + | var templ_7745c5c3_Var167 templ.SafeURL |
| 3163 | - | templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | 3195 | + | templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) |
| 3164 | if templ_7745c5c3_Err != nil { | 3196 | if templ_7745c5c3_Err != nil { | ||
| 3165 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 38} | 3197 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 38} |
| 3166 | } | 3198 | } | ||
| 3167 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165)) | 3199 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167)) |
| 3168 | if templ_7745c5c3_Err != nil { | 3200 | if templ_7745c5c3_Err != nil { | ||
| 3169 | return templ_7745c5c3_Err | 3201 | return templ_7745c5c3_Err | ||
| 3170 | } | 3202 | } | ||
| 3171 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">") | 3203 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "\">") |
| 3172 | if templ_7745c5c3_Err != nil { | 3204 | if templ_7745c5c3_Err != nil { | ||
| 3173 | return templ_7745c5c3_Err | 3205 | return templ_7745c5c3_Err | ||
| 3174 | } | 3206 | } | ||
| 3175 | - | var templ_7745c5c3_Var166 string | 3207 | + | var templ_7745c5c3_Var168 string |
| 3176 | - | templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | 3208 | + | templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) |
| 3177 | if templ_7745c5c3_Err != nil { | 3209 | if templ_7745c5c3_Err != nil { | ||
| 3178 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 49} | 3210 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 49} |
| 3179 | } | 3211 | } | ||
| 3180 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166)) | 3212 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168)) |
| 3181 | if templ_7745c5c3_Err != nil { | 3213 | if templ_7745c5c3_Err != nil { | ||
| 3182 | return templ_7745c5c3_Err | 3214 | return templ_7745c5c3_Err | ||
| 3183 | } | 3215 | } | ||
| 3184 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</a> ") | 3216 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</a> ") |
| 3185 | if templ_7745c5c3_Err != nil { | 3217 | if templ_7745c5c3_Err != nil { | ||
| 3186 | return templ_7745c5c3_Err | 3218 | return templ_7745c5c3_Err | ||
| 3187 | } | 3219 | } | ||
| 3188 | } else { | 3220 | } else { | ||
| 3189 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span>") | 3221 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "<span>") |
| 3190 | if templ_7745c5c3_Err != nil { | 3222 | if templ_7745c5c3_Err != nil { | ||
| 3191 | return templ_7745c5c3_Err | 3223 | return templ_7745c5c3_Err | ||
| 3192 | } | 3224 | } | ||
| 3193 | - | var templ_7745c5c3_Var167 string | 3225 | + | var templ_7745c5c3_Var169 string |
| 3194 | - | templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | 3226 | + | templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) |
| 3195 | if templ_7745c5c3_Err != nil { | 3227 | if templ_7745c5c3_Err != nil { | ||
| 3196 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 464, Col: 21} | 3228 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 467, Col: 21} |
| 3197 | } | 3229 | } | ||
| 3198 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167)) | 3230 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169)) |
| 3199 | if templ_7745c5c3_Err != nil { | 3231 | if templ_7745c5c3_Err != nil { | ||
| 3200 | return templ_7745c5c3_Err | 3232 | return templ_7745c5c3_Err | ||
| 3201 | } | 3233 | } | ||
| 3202 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span> ") | 3234 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</span> ") |
| 3203 | if templ_7745c5c3_Err != nil { | 3235 | if templ_7745c5c3_Err != nil { | ||
| 3204 | return templ_7745c5c3_Err | 3236 | return templ_7745c5c3_Err | ||
| 3205 | } | 3237 | } | ||
| 3206 | } | 3238 | } | ||
| 3207 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<span class=\"dim\">") | 3239 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "<span class=\"dim\">") |
| 3208 | if templ_7745c5c3_Err != nil { | 3240 | if templ_7745c5c3_Err != nil { | ||
| 3209 | return templ_7745c5c3_Err | 3241 | return templ_7745c5c3_Err | ||
| 3210 | } | 3242 | } | ||
| 3211 | - | var templ_7745c5c3_Var168 string | 3243 | + | var templ_7745c5c3_Var170 string |
| 3212 | - | templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.State) | 3244 | + | templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(r.State) |
| 3213 | if templ_7745c5c3_Err != nil { | 3245 | if templ_7745c5c3_Err != nil { | ||
| 3214 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 466, Col: 33} | 3246 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 469, Col: 33} |
| 3215 | } | 3247 | } | ||
| 3216 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168)) | 3248 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170)) |
| 3217 | if templ_7745c5c3_Err != nil { | 3249 | if templ_7745c5c3_Err != nil { | ||
| 3218 | return templ_7745c5c3_Err | 3250 | return templ_7745c5c3_Err | ||
| 3219 | } | 3251 | } | ||
| 3220 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> ") | 3252 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</span> ") |
| 3221 | if templ_7745c5c3_Err != nil { | 3253 | if templ_7745c5c3_Err != nil { | ||
| 3222 | return templ_7745c5c3_Err | 3254 | return templ_7745c5c3_Err | ||
| 3223 | } | 3255 | } | ||
| 3224 | if r.Waiting { | 3256 | if r.Waiting { | ||
| 3225 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<span class=\"mute\">waiting for a runner</span>") | 3257 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<span class=\"mute\">waiting for a runner</span>") |
| 3226 | if templ_7745c5c3_Err != nil { | 3258 | if templ_7745c5c3_Err != nil { | ||
| 3227 | return templ_7745c5c3_Err | 3259 | return templ_7745c5c3_Err | ||
| 3228 | } | 3260 | } | ||
| 3229 | } | 3261 | } | ||
| 3230 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</li>") | 3262 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</li>") |
| 3231 | if templ_7745c5c3_Err != nil { | 3263 | if templ_7745c5c3_Err != nil { | ||
| 3232 | return templ_7745c5c3_Err | 3264 | return templ_7745c5c3_Err | ||
| 3233 | } | 3265 | } | ||
| 3234 | } | 3266 | } | ||
| 3235 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>") | 3267 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</ul>") |
| 3236 | if templ_7745c5c3_Err != nil { | 3268 | if templ_7745c5c3_Err != nil { | ||
| 3237 | return templ_7745c5c3_Err | 3269 | return templ_7745c5c3_Err | ||
| 3238 | } | 3270 | } | ||
| 3239 | } | 3271 | } | ||
| 3240 | if c.Note != "" { | 3272 | if c.Note != "" { | ||
| 3241 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"rule-note\"><span class=\"sr\">note: </span>") | 3273 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<p class=\"rule-note\"><span class=\"sr\">note: </span>") |
| 3242 | if templ_7745c5c3_Err != nil { | 3274 | if templ_7745c5c3_Err != nil { | ||
| 3243 | return templ_7745c5c3_Err | 3275 | return templ_7745c5c3_Err | ||
| 3244 | } | 3276 | } | ||
| 3245 | - | var templ_7745c5c3_Var169 string | 3277 | + | var templ_7745c5c3_Var171 string |
| 3246 | - | templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note) | 3278 | + | templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note) |
| 3247 | if templ_7745c5c3_Err != nil { | 3279 | if templ_7745c5c3_Err != nil { | ||
| 3248 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 475, Col: 62} | 3280 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 478, Col: 62} |
| 3249 | } | 3281 | } | ||
| 3250 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169)) | 3282 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171)) |
| 3251 | if templ_7745c5c3_Err != nil { | 3283 | if templ_7745c5c3_Err != nil { | ||
| 3252 | return templ_7745c5c3_Err | 3284 | return templ_7745c5c3_Err | ||
| 3253 | } | 3285 | } | ||
| 3254 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>") | 3286 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "</p>") |
| 3255 | if templ_7745c5c3_Err != nil { | 3287 | if templ_7745c5c3_Err != nil { | ||
| 3256 | return templ_7745c5c3_Err | 3288 | return templ_7745c5c3_Err | ||
| 3257 | } | 3289 | } | ||
| 3258 | } | 3290 | } | ||
| 3259 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</section>") | 3291 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</section>") |
| 3260 | if templ_7745c5c3_Err != nil { | 3292 | if templ_7745c5c3_Err != nil { | ||
| 3261 | return templ_7745c5c3_Err | 3293 | return templ_7745c5c3_Err | ||
| 3262 | } | 3294 | } | ||
M internal/adapters/web/checks.go +38 -10
| @@ -25,6 +25,7 @@ type ChecksSummary struct { | |||
| 25 | 25 | type CheckRow struct { | |
| 26 | 26 | Name, Kind, State, Href string | |
| 27 | 27 | Waiting bool | |
| 28 | + | GroupLabel, GroupHref string | |
| 28 | 29 | } | |
| 29 | 30 | ||
| 30 | 31 | // ChecksSection is the checks of the head of a merge request. | |
| @@ -82,17 +83,28 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C | |||
| 82 | 83 | // the repository that applies to them. | |
| 83 | 84 | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |
| 84 | 85 | sec := ChecksSection{} | |
| 85 | - | for _, c := range checks { | |
| 86 | - | row := CheckRow{ | |
| 87 | - | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | |
| 88 | - | Waiting: c.WaitingForRunner, | |
| 89 | - | } | |
| 90 | - | if c.JobID != "" { | |
| 91 | - | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | |
| 92 | - | } else { | |
| 93 | - | row.Href = runHref(repo.Name.String(), c.RunID) | |
| 86 | + | groups := checksByRun(checks) | |
| 87 | + | for _, group := range groups { | |
| 88 | + | for i, c := range group { | |
| 89 | + | row := CheckRow{ | |
| 90 | + | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | |
| 91 | + | Waiting: c.WaitingForRunner, | |
| 92 | + | } | |
| 93 | + | if c.JobID != "" { | |
| 94 | + | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | |
| 95 | + | } else { | |
| 96 | + | row.Href = runHref(repo.Name.String(), c.RunID) | |
| 97 | + | } | |
| 98 | + | if len(groups) > 1 && i == 0 { | |
| 99 | + | event := visibleText(c.Event) | |
| 100 | + | if event == "" { | |
| 101 | + | event = fmt.Sprintf("run %d", c.RunID) | |
| 102 | + | } | |
| 103 | + | row.GroupLabel = visibleText(c.WorkflowName) + " · " + event | |
| 104 | + | row.GroupHref = runHref(repo.Name.String(), c.RunID) | |
| 105 | + | } | |
| 106 | + | sec.Rows = append(sec.Rows, row) | |
| 94 | 107 | } | |
| 95 | - | sec.Rows = append(sec.Rows, row) | |
| 96 | 108 | } | |
| 97 | 109 | switch { | |
| 98 | 110 | case len(checks) == 0: | |
| @@ -104,3 +116,19 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |||
| 104 | 116 | } | |
| 105 | 117 | return sec | |
| 106 | 118 | } | |
| 119 | + | ||
| 120 | + | // checksByRun keeps first-appearance order and the check order within each run. | |
| 121 | + | func checksByRun(checks []ports.CheckView) [][]ports.CheckView { | |
| 122 | + | groups := make([][]ports.CheckView, 0) | |
| 123 | + | indices := make(map[int64]int) | |
| 124 | + | for _, c := range checks { | |
| 125 | + | i, ok := indices[c.RunID] | |
| 126 | + | if !ok { | |
| 127 | + | i = len(groups) | |
| 128 | + | indices[c.RunID] = i | |
| 129 | + | groups = append(groups, nil) | |
| 130 | + | } | |
| 131 | + | groups[i] = append(groups[i], c) | |
| 132 | + | } | |
| 133 | + | return groups | |
| 134 | + | } | |
| @@ -25,6 +25,7 @@ type ChecksSummary struct { | |||||
| 25 | type CheckRow struct { | 25 | type CheckRow struct { | ||
| 26 | Name, Kind, State, Href string | 26 | Name, Kind, State, Href string | ||
| 27 | Waiting bool | 27 | Waiting bool | ||
| 28 | + | GroupLabel, GroupHref string | |||
| 28 | } | 29 | } | ||
| 29 | 30 | ||||
| 30 | // ChecksSection is the checks of the head of a merge request. | 31 | // ChecksSection is the checks of the head of a merge request. | ||
| @@ -82,17 +83,28 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C | |||||
| 82 | // the repository that applies to them. | 83 | // the repository that applies to them. | ||
| 83 | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | 84 | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | ||
| 84 | sec := ChecksSection{} | 85 | sec := ChecksSection{} | ||
| 85 | - | for _, c := range checks { | 86 | + | groups := checksByRun(checks) |
| 86 | - | row := CheckRow{ | 87 | + | for _, group := range groups { |
| 87 | - | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | 88 | + | for i, c := range group { |
| 88 | - | Waiting: c.WaitingForRunner, | 89 | + | row := CheckRow{ |
| 89 | - | } | 90 | + | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), |
| 90 | - | if c.JobID != "" { | 91 | + | Waiting: c.WaitingForRunner, |
| 91 | - | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | 92 | + | } |
| 92 | - | } else { | 93 | + | if c.JobID != "" { |
| 93 | - | row.Href = runHref(repo.Name.String(), c.RunID) | 94 | + | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) |
| 95 | + | } else { | |||
| 96 | + | row.Href = runHref(repo.Name.String(), c.RunID) | |||
| 97 | + | } | |||
| 98 | + | if len(groups) > 1 && i == 0 { | |||
| 99 | + | event := visibleText(c.Event) | |||
| 100 | + | if event == "" { | |||
| 101 | + | event = fmt.Sprintf("run %d", c.RunID) | |||
| 102 | + | } | |||
| 103 | + | row.GroupLabel = visibleText(c.WorkflowName) + " · " + event | |||
| 104 | + | row.GroupHref = runHref(repo.Name.String(), c.RunID) | |||
| 105 | + | } | |||
| 106 | + | sec.Rows = append(sec.Rows, row) | |||
| 94 | } | 107 | } | ||
| 95 | - | sec.Rows = append(sec.Rows, row) | |||
| 96 | } | 108 | } | ||
| 97 | switch { | 109 | switch { | ||
| 98 | case len(checks) == 0: | 110 | case len(checks) == 0: | ||
| @@ -104,3 +116,19 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |||||
| 104 | } | 116 | } | ||
| 105 | return sec | 117 | return sec | ||
| 106 | } | 118 | } | ||
| 119 | + | ||||
| 120 | + | // checksByRun keeps first-appearance order and the check order within each run. | |||
| 121 | + | func checksByRun(checks []ports.CheckView) [][]ports.CheckView { | |||
| 122 | + | groups := make([][]ports.CheckView, 0) | |||
| 123 | + | indices := make(map[int64]int) | |||
| 124 | + | for _, c := range checks { | |||
| 125 | + | i, ok := indices[c.RunID] | |||
| 126 | + | if !ok { | |||
| 127 | + | i = len(groups) | |||
| 128 | + | indices[c.RunID] = i | |||
| 129 | + | groups = append(groups, nil) | |||
| 130 | + | } | |||
| 131 | + | groups[i] = append(groups[i], c) | |||
| 132 | + | } | |||
| 133 | + | return groups | |||
| 134 | + | } | |||
A internal/adapters/web/checks_test.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "context" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ||
| 13 | + | "bnema.dev/ermite/internal/domain" | |
| 14 | + | "bnema.dev/ermite/internal/ports" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | func TestChecksSectionRuns(t *testing.T) { | |
| 18 | + | t.Parallel() | |
| 19 | + | repo := domain.Repo{Name: repoName(t, "proj")} | |
| 20 | + | check := func(run int64, name, event string) ports.CheckView { | |
| 21 | + | return ports.CheckView{CICheck: domain.CICheck{RunID: run, Name: name, WorkflowName: "Smoke", Status: domain.CICompleted, Conclusion: domain.CISuccess}, Event: event} | |
| 22 | + | } | |
| 23 | + | a, b, c := check(7, "build", "push"), check(8, "build", "pull_request"), check(7, "test", "push") | |
| 24 | + | single := checksSection(repo, []ports.CheckView{a, c}) | |
| 25 | + | require.Len(t, single.Rows, 2) | |
| 26 | + | assert.Equal(t, []CheckRow{ | |
| 27 | + | {Name: "build", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |
| 28 | + | {Name: "test", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |
| 29 | + | }, single.Rows) | |
| 30 | + | var flat bytes.Buffer | |
| 31 | + | require.NoError(t, MRChecks(single).Render(context.Background(), &flat)) | |
| 32 | + | assert.NotContains(t, flat.String(), "ck-group") | |
| 33 | + | ||
| 34 | + | multi := checksSection(repo, []ports.CheckView{a, b, c}) | |
| 35 | + | require.Len(t, multi.Rows, 3) | |
| 36 | + | assert.Equal(t, "Smoke · push", multi.Rows[0].GroupLabel) | |
| 37 | + | assert.Equal(t, "test", multi.Rows[1].Name) | |
| 38 | + | assert.Empty(t, multi.Rows[1].GroupLabel) | |
| 39 | + | assert.Equal(t, "Smoke · pull_request", multi.Rows[2].GroupLabel) | |
| 40 | + | assert.Equal(t, runHref("proj", 8), multi.Rows[2].GroupHref) | |
| 41 | + | var html bytes.Buffer | |
| 42 | + | require.NoError(t, MRChecks(multi).Render(context.Background(), &html)) | |
| 43 | + | assert.Contains(t, html.String(), `Checks <span class="count">3</span>`) | |
| 44 | + | assert.Contains(t, html.String(), `<li class="ck-group"><a href="`+runHref("proj", 7)+`">Smoke · push</a></li>`) | |
| 45 | + | t.Log(html.String()) | |
| 46 | + | b.Event = "" | |
| 47 | + | fallback := checksSection(repo, []ports.CheckView{a, b}) | |
| 48 | + | assert.Equal(t, "Smoke · run 8", fallback.Rows[1].GroupLabel) | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "context" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ||||
| 13 | + | "bnema.dev/ermite/internal/domain" | |||
| 14 | + | "bnema.dev/ermite/internal/ports" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | func TestChecksSectionRuns(t *testing.T) { | |||
| 18 | + | t.Parallel() | |||
| 19 | + | repo := domain.Repo{Name: repoName(t, "proj")} | |||
| 20 | + | check := func(run int64, name, event string) ports.CheckView { | |||
| 21 | + | return ports.CheckView{CICheck: domain.CICheck{RunID: run, Name: name, WorkflowName: "Smoke", Status: domain.CICompleted, Conclusion: domain.CISuccess}, Event: event} | |||
| 22 | + | } | |||
| 23 | + | a, b, c := check(7, "build", "push"), check(8, "build", "pull_request"), check(7, "test", "push") | |||
| 24 | + | single := checksSection(repo, []ports.CheckView{a, c}) | |||
| 25 | + | require.Len(t, single.Rows, 2) | |||
| 26 | + | assert.Equal(t, []CheckRow{ | |||
| 27 | + | {Name: "build", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |||
| 28 | + | {Name: "test", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |||
| 29 | + | }, single.Rows) | |||
| 30 | + | var flat bytes.Buffer | |||
| 31 | + | require.NoError(t, MRChecks(single).Render(context.Background(), &flat)) | |||
| 32 | + | assert.NotContains(t, flat.String(), "ck-group") | |||
| 33 | + | ||||
| 34 | + | multi := checksSection(repo, []ports.CheckView{a, b, c}) | |||
| 35 | + | require.Len(t, multi.Rows, 3) | |||
| 36 | + | assert.Equal(t, "Smoke · push", multi.Rows[0].GroupLabel) | |||
| 37 | + | assert.Equal(t, "test", multi.Rows[1].Name) | |||
| 38 | + | assert.Empty(t, multi.Rows[1].GroupLabel) | |||
| 39 | + | assert.Equal(t, "Smoke · pull_request", multi.Rows[2].GroupLabel) | |||
| 40 | + | assert.Equal(t, runHref("proj", 8), multi.Rows[2].GroupHref) | |||
| 41 | + | var html bytes.Buffer | |||
| 42 | + | require.NoError(t, MRChecks(multi).Render(context.Background(), &html)) | |||
| 43 | + | assert.Contains(t, html.String(), `Checks <span class="count">3</span>`) | |||
| 44 | + | assert.Contains(t, html.String(), `<li class="ck-group"><a href="`+runHref("proj", 7)+`">Smoke · push</a></li>`) | |||
| 45 | + | t.Log(html.String()) | |||
| 46 | + | b.Event = "" | |||
| 47 | + | fallback := checksSection(repo, []ports.CheckView{a, b}) | |||
| 48 | + | assert.Equal(t, "Smoke · run 8", fallback.Rows[1].GroupLabel) | |||
| 49 | + | } | |||
M internal/adapters/web/static/css/65-ci.css +3 -0
| @@ -60,6 +60,9 @@ | |||
| 60 | 60 | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | |
| 61 | 61 | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | |
| 62 | 62 | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | |
| 63 | + | .checks .ck-group { font-size: var(--fs-xs); color: var(--ink-dim); margin-top: var(--s2); } | |
| 64 | + | .checks .ck-group:first-child { margin-top: 0; } | |
| 65 | + | .checks .ck-group a { color: inherit; } | |
| 63 | 66 | /* the merge rule under the checks: a note about them, not one of them */ | |
| 64 | 67 | .rail section > .rule-note { | |
| 65 | 68 | display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border); | |
| @@ -60,6 +60,9 @@ | |||||
| 60 | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | 60 | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | ||
| 61 | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | 61 | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | ||
| 62 | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | 62 | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | ||
| 63 | + | .checks .ck-group { font-size: var(--fs-xs); color: var(--ink-dim); margin-top: var(--s2); } | |||
| 64 | + | .checks .ck-group:first-child { margin-top: 0; } | |||
| 65 | + | .checks .ck-group a { color: inherit; } | |||
| 63 | /* the merge rule under the checks: a note about them, not one of them */ | 66 | /* the merge rule under the checks: a note about them, not one of them */ | ||
| 64 | .rail section > .rule-note { | 67 | .rail section > .rule-note { | ||
| 65 | display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border); | 68 | display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border); | ||
M internal/ports/ci.go +2 -0
| @@ -695,6 +695,8 @@ type GetJobLogParams struct { | |||
| 695 | 695 | // CheckView is a check with what owners need to read it. | |
| 696 | 696 | type CheckView struct { | |
| 697 | 697 | domain.CICheck | |
| 698 | + | // Event is the event of the run of the check (push, pull_request, ...), empty if unknown. | |
| 699 | + | Event string | |
| 698 | 700 | // WaitingForRunner is set for a queued check whose job no registered | |
| 699 | 701 | // runner can take (no runner has all its labels). | |
| 700 | 702 | WaitingForRunner bool | |
| @@ -695,6 +695,8 @@ type GetJobLogParams struct { | |||||
| 695 | // CheckView is a check with what owners need to read it. | 695 | // CheckView is a check with what owners need to read it. | ||
| 696 | type CheckView struct { | 696 | type CheckView struct { | ||
| 697 | domain.CICheck | 697 | domain.CICheck | ||
| 698 | + | // Event is the event of the run of the check (push, pull_request, ...), empty if unknown. | |||
| 699 | + | Event string | |||
| 698 | // WaitingForRunner is set for a queued check whose job no registered | 700 | // WaitingForRunner is set for a queued check whose job no registered | ||
| 699 | // runner can take (no runner has all its labels). | 701 | // runner can take (no runner has all its labels). | ||
| 700 | WaitingForRunner bool | 702 | WaitingForRunner bool | ||
M internal/usecase/ci_runner_unit_test.go +39 -0
| @@ -421,3 +421,42 @@ func TestLogLimitEndsTheJob(t *testing.T) { | |||
| 421 | 421 | assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion) | |
| 422 | 422 | assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job") | |
| 423 | 423 | } | |
| 424 | + | ||
| 425 | + | func TestListChecksRunEvents(t *testing.T) { | |
| 426 | + | t.Parallel() | |
| 427 | + | for _, tc := range []struct { | |
| 428 | + | name string | |
| 429 | + | err error | |
| 430 | + | }{ | |
| 431 | + | {name: "events"}, {name: "pruned", err: domain.ErrNotFound}, {name: "store error", err: errors.New("store unavailable")}, | |
| 432 | + | } { | |
| 433 | + | t.Run(tc.name, func(t *testing.T) { | |
| 434 | + | t.Parallel() | |
| 435 | + | u := newCIUnit(t, nil) | |
| 436 | + | repo := name(t, "proj") | |
| 437 | + | sha := "0123456789012345678901234567890123456789" | |
| 438 | + | u.meta.EXPECT().Get(mock.Anything, repo).Return(domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}, nil) | |
| 439 | + | u.store.EXPECT().ListChecks(mock.Anything, repo, sha, usecase.MaxCIChecks).Return([]domain.CICheck{ | |
| 440 | + | {RunID: 7, Status: domain.CICompleted}, {RunID: 8, Status: domain.CICompleted}, {RunID: 7, Status: domain.CICompleted}, | |
| 441 | + | }, false, nil).Once() | |
| 442 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(7)).Return(domain.CIRun{Event: "push"}, tc.err).Once() | |
| 443 | + | if tc.err == nil || errors.Is(tc.err, domain.ErrNotFound) { | |
| 444 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(8)).Return(domain.CIRun{Event: "pull_request"}, nil).Once() | |
| 445 | + | } | |
| 446 | + | res, err := u.ci.ListChecks(context.Background(), domain.Anonymous(), repo, sha) | |
| 447 | + | if tc.err != nil && !errors.Is(tc.err, domain.ErrNotFound) { | |
| 448 | + | require.ErrorIs(t, err, tc.err) | |
| 449 | + | return | |
| 450 | + | } | |
| 451 | + | require.NoError(t, err) | |
| 452 | + | require.Len(t, res.Checks, 3) | |
| 453 | + | event := "push" | |
| 454 | + | if tc.err != nil { | |
| 455 | + | event = "" | |
| 456 | + | } | |
| 457 | + | assert.Equal(t, event, res.Checks[0].Event) | |
| 458 | + | assert.Equal(t, "pull_request", res.Checks[1].Event) | |
| 459 | + | assert.Equal(t, event, res.Checks[2].Event) | |
| 460 | + | }) | |
| 461 | + | } | |
| 462 | + | } | |
| @@ -421,3 +421,42 @@ func TestLogLimitEndsTheJob(t *testing.T) { | |||||
| 421 | assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion) | 421 | assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion) | ||
| 422 | assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job") | 422 | assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job") | ||
| 423 | } | 423 | } | ||
| 424 | + | ||||
| 425 | + | func TestListChecksRunEvents(t *testing.T) { | |||
| 426 | + | t.Parallel() | |||
| 427 | + | for _, tc := range []struct { | |||
| 428 | + | name string | |||
| 429 | + | err error | |||
| 430 | + | }{ | |||
| 431 | + | {name: "events"}, {name: "pruned", err: domain.ErrNotFound}, {name: "store error", err: errors.New("store unavailable")}, | |||
| 432 | + | } { | |||
| 433 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 434 | + | t.Parallel() | |||
| 435 | + | u := newCIUnit(t, nil) | |||
| 436 | + | repo := name(t, "proj") | |||
| 437 | + | sha := "0123456789012345678901234567890123456789" | |||
| 438 | + | u.meta.EXPECT().Get(mock.Anything, repo).Return(domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}, nil) | |||
| 439 | + | u.store.EXPECT().ListChecks(mock.Anything, repo, sha, usecase.MaxCIChecks).Return([]domain.CICheck{ | |||
| 440 | + | {RunID: 7, Status: domain.CICompleted}, {RunID: 8, Status: domain.CICompleted}, {RunID: 7, Status: domain.CICompleted}, | |||
| 441 | + | }, false, nil).Once() | |||
| 442 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(7)).Return(domain.CIRun{Event: "push"}, tc.err).Once() | |||
| 443 | + | if tc.err == nil || errors.Is(tc.err, domain.ErrNotFound) { | |||
| 444 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(8)).Return(domain.CIRun{Event: "pull_request"}, nil).Once() | |||
| 445 | + | } | |||
| 446 | + | res, err := u.ci.ListChecks(context.Background(), domain.Anonymous(), repo, sha) | |||
| 447 | + | if tc.err != nil && !errors.Is(tc.err, domain.ErrNotFound) { | |||
| 448 | + | require.ErrorIs(t, err, tc.err) | |||
| 449 | + | return | |||
| 450 | + | } | |||
| 451 | + | require.NoError(t, err) | |||
| 452 | + | require.Len(t, res.Checks, 3) | |||
| 453 | + | event := "push" | |||
| 454 | + | if tc.err != nil { | |||
| 455 | + | event = "" | |||
| 456 | + | } | |||
| 457 | + | assert.Equal(t, event, res.Checks[0].Event) | |||
| 458 | + | assert.Equal(t, "pull_request", res.Checks[1].Event) | |||
| 459 | + | assert.Equal(t, event, res.Checks[2].Event) | |||
| 460 | + | }) | |||
| 461 | + | } | |||
| 462 | + | } | |||
M internal/usecase/ci_runs.go +13 -1
| @@ -519,8 +519,20 @@ func (c *CI) ListChecks(ctx context.Context, p domain.Principal, name domain.Rep | |||
| 519 | 519 | } | |
| 520 | 520 | res := ports.ChecksResult{Truncated: more} | |
| 521 | 521 | var runners []domain.CIRunner | |
| 522 | + | events := make(map[int64]string) | |
| 522 | 523 | for _, ck := range checks { | |
| 523 | - | v := ports.CheckView{CICheck: ck} | |
| 524 | + | event, ok := events[ck.RunID] | |
| 525 | + | if !ok { | |
| 526 | + | run, err := c.cfg.Store.GetRun(ctx, name, ck.RunID) | |
| 527 | + | if err != nil && !errors.Is(err, domain.ErrNotFound) { | |
| 528 | + | return ports.ChecksResult{}, err | |
| 529 | + | } | |
| 530 | + | if err == nil { | |
| 531 | + | event = run.Event | |
| 532 | + | } | |
| 533 | + | events[ck.RunID] = event | |
| 534 | + | } | |
| 535 | + | v := ports.CheckView{CICheck: ck, Event: event} | |
| 524 | 536 | if ck.Status == domain.CIQueued { | |
| 525 | 537 | if runners == nil { | |
| 526 | 538 | runners, _ = c.cfg.Store.ListRunners(ctx) | |
| @@ -519,8 +519,20 @@ func (c *CI) ListChecks(ctx context.Context, p domain.Principal, name domain.Rep | |||||
| 519 | } | 519 | } | ||
| 520 | res := ports.ChecksResult{Truncated: more} | 520 | res := ports.ChecksResult{Truncated: more} | ||
| 521 | var runners []domain.CIRunner | 521 | var runners []domain.CIRunner | ||
| 522 | + | events := make(map[int64]string) | |||
| 522 | for _, ck := range checks { | 523 | for _, ck := range checks { | ||
| 523 | - | v := ports.CheckView{CICheck: ck} | 524 | + | event, ok := events[ck.RunID] |
| 525 | + | if !ok { | |||
| 526 | + | run, err := c.cfg.Store.GetRun(ctx, name, ck.RunID) | |||
| 527 | + | if err != nil && !errors.Is(err, domain.ErrNotFound) { | |||
| 528 | + | return ports.ChecksResult{}, err | |||
| 529 | + | } | |||
| 530 | + | if err == nil { | |||
| 531 | + | event = run.Event | |||
| 532 | + | } | |||
| 533 | + | events[ck.RunID] = event | |||
| 534 | + | } | |||
| 535 | + | v := ports.CheckView{CICheck: ck, Event: event} | |||
| 524 | if ck.Status == domain.CIQueued { | 536 | if ck.Status == domain.CIQueued { | ||
| 525 | if runners == nil { | 537 | if runners == nil { | ||
| 526 | runners, _ = c.cfg.Store.ListRunners(ctx) | 538 | runners, _ = c.cfg.Store.ListRunners(ctx) | ||