repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/web/view.go
Rawpackage web
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"bugabinga.net/luci/internal/history"
"bugabinga.net/luci/internal/inbox"
"bugabinga.net/luci/internal/proquint"
"bugabinga.net/luci/internal/runstate"
"bugabinga.net/luci/internal/trigger"
)
type dashboardView struct {
FailingRepos, RunningRepos, HealthyRepos int
Attention, Progress, Success []runView
Warnings []string
All, Running bool
}
type runView struct {
RunID, ChildID, Repo, Job, Status, Ref, FullRef, Rev, FullRev, FullHead, FullTail, Trigger, Timing, DurationSeconds, Age, Detail, Search, Started, StartedHuman, Finished, FinishedHuman, Meta, When, Subject, Wait, WaitSeconds, Matrix, Pronounce, PronounceShort, PronounceHead, PronounceTail string
MatrixValues map[string]string
SortTime time.Time
StartedAt, EndedAt time.Time
}
type runDetailView struct {
Known bool
Parent runView
Children []runView
}
type Dashboard = dashboardView
type Run = runView
type RunDetail = runDetailView
func BuildDashboard(now time.Time, events []history.Event, pending []inbox.Pending, active []runstate.Active, all bool) Dashboard {
return buildDashboard(now, events, pending, active, all)
}
func BuildRun(runID string, now time.Time, events []history.Event, pending []inbox.Pending, active []runstate.Active) RunDetail {
return buildRun(runID, now, events, pending, active)
}
func buildDashboard(now time.Time, events []history.Event, pending []inbox.Pending, active []runstate.Active, all bool) dashboardView {
parents := parentEvents(events)
latest := map[string]history.Event{}
for _, event := range parents {
if previous, ok := latest[event.Repo]; !ok || eventAfter(event, previous) {
latest[event.Repo] = event
}
}
activeRuns, progressRepos := map[string]runstate.Active{}, map[string]bool{}
pendingRuns := map[string]inbox.Pending{}
for _, current := range pending {
pendingRuns[current.ID] = current
}
var progress []runView
for _, current := range active {
activeRuns[current.RunID] = current
progressRepos[current.Repo] = true
if parent, ok := pendingRuns[current.RunID]; ok {
progress = append(progress, runningParent(now, parent, current))
} else {
progress = append(progress, activeView(now, current))
}
}
for _, current := range pending {
if _, running := activeRuns[current.ID]; running {
continue
}
v := pendingView(now, current)
progressRepos[v.Repo] = true
progress = append(progress, v)
}
var attention, success []runView
for _, event := range latest {
if event.Status == "failed" {
attention = append(attention, eventView(now, event))
}
}
for _, event := range parents {
if event.Status == "success" {
success = append(success, eventView(now, event))
}
}
repos := map[string]bool{}
for repo := range latest {
repos[repo] = true
}
for repo := range progressRepos {
repos[repo] = true
}
view := dashboardView{Attention: attention, Progress: progress, Success: success, All: all}
for repo := range repos {
terminal, ok := latest[repo]
switch {
case ok && terminal.Status == "failed":
view.FailingRepos++
case progressRepos[repo]:
view.RunningRepos++
case ok && terminal.Status == "success":
view.HealthyRepos++
}
}
sort.Slice(view.Attention, func(i, j int) bool {
if view.Attention[i].SortTime.Equal(view.Attention[j].SortTime) {
return view.Attention[i].RunID > view.Attention[j].RunID
}
return view.Attention[i].SortTime.After(view.Attention[j].SortTime)
})
sort.Slice(view.Success, func(i, j int) bool {
if view.Success[i].SortTime.Equal(view.Success[j].SortTime) {
return view.Success[i].RunID > view.Success[j].RunID
}
return view.Success[i].SortTime.After(view.Success[j].SortTime)
})
sort.Slice(view.Progress, func(i, j int) bool {
a, b := submitted(view.Progress[i].RunID), submitted(view.Progress[j].RunID)
if a.Equal(b) {
return view.Progress[i].RunID > view.Progress[j].RunID
}
return a.After(b)
})
if !all {
view.Attention = limit(view.Attention, 50)
view.Progress = limit(view.Progress, 50)
view.Success = limit(view.Success, 50)
}
short := buildShortener(events, pending, active)
for i := range view.Attention {
short.apply(&view.Attention[i])
}
for i := range view.Progress {
short.apply(&view.Progress[i])
}
for i := range view.Success {
short.apply(&view.Success[i])
}
return view
}
func buildRun(runID string, now time.Time, events []history.Event, pending []inbox.Pending, active []runstate.Active) runDetailView {
var out runDetailView
explicitParent := false
var legacy []history.Event
for _, event := range events {
if event.RunID != runID {
continue
}
out.Known = true
view := eventView(now, event)
switch event.ChildID {
case "run":
out.Parent = view
explicitParent = true
case "":
legacy = append(legacy, event)
default:
out.Children = append(out.Children, view)
}
}
if !explicitParent && len(legacy) > 0 {
sort.SliceStable(legacy, func(i, j int) bool {
if legacy[i].Job == legacy[j].Job {
return legacy[i].Time.Before(legacy[j].Time)
}
return legacy[i].Job < legacy[j].Job
})
latestLegacy := legacy[0]
for i, event := range legacy {
child := eventView(now, event)
child.ChildID = fmt.Sprintf("legacy-%03d", i+1)
out.Children = append(out.Children, child)
if eventAfter(event, latestLegacy) {
latestLegacy = event
}
}
out.Parent = eventView(now, latestLegacy)
out.Parent.ChildID = "run"
}
var parentPending *inbox.Pending
if !explicitParent {
for i := range pending {
if pending[i].ID == runID {
parentPending = &pending[i]
out.Known = true
out.Parent = pendingView(now, pending[i])
break
}
}
}
for _, current := range active {
if current.RunID != runID {
continue
}
out.Known = true
child := activeView(now, current)
out.Children = append(out.Children, child)
if !explicitParent {
if parentPending != nil {
out.Parent = runningParent(now, *parentPending, current)
} else {
out.Parent = child
out.Parent.ChildID = "run"
}
}
}
if out.Parent.RunID == "" && len(out.Children) > 0 {
out.Parent = out.Children[0]
out.Parent.ChildID = "run"
}
if !out.Parent.StartedAt.IsZero() {
for i := range out.Children {
if out.Children[i].StartedAt.After(out.Parent.StartedAt) {
if wait := out.Children[i].StartedAt.Sub(out.Parent.StartedAt); wait > time.Second {
out.Children[i].Wait = "queued " + compactDuration(wait)
out.Children[i].WaitSeconds = strconv.FormatInt(int64(wait/time.Second), 10)
}
}
}
}
short := buildShortener(events, pending, active)
short.apply(&out.Parent)
for i := range out.Children {
short.apply(&out.Children[i])
}
sort.Slice(out.Children, func(i, j int) bool { return out.Children[i].ChildID < out.Children[j].ChildID })
return out
}
func parentEvents(events []history.Event) []history.Event {
explicit := map[string]bool{}
for _, e := range events {
if e.ChildID == "run" {
explicit[e.RunID] = true
}
}
var out []history.Event
for _, e := range events {
if e.ChildID == "run" || e.ChildID == "" && !explicit[e.RunID] {
out = append(out, e)
}
}
return out
}
func eventAfter(a, b history.Event) bool {
if a.Time.Equal(b.Time) {
return a.RunID > b.RunID
}
return a.Time.After(b.Time)
}
func pronounces(id string) string {
if value, err := strconv.ParseInt(id, 10, 64); err == nil {
return proquint.Encode(value)
}
return ""
}
func eventView(now time.Time, e history.Event) runView {
v := runView{RunID: e.RunID, Pronounce: pronounces(e.RunID), ChildID: e.ChildID, Repo: e.Repo, Job: e.Job, Status: e.Status, Subject: e.Subject, Ref: shortRef(e.Ref), FullRef: valueOrDash(e.Ref), Rev: shortRev(e.Rev), FullRev: valueOrDash(e.Rev), Trigger: triggerDisplay(e.Trigger), Detail: e.Detail, Matrix: matrixDisplay(e.Matrix), MatrixValues: e.Matrix, Age: age(now, e.Time), Finished: timestamp(e.Time), FinishedHuman: humanTimestamp(e.Time), SortTime: e.Time, StartedAt: e.StartedAt, EndedAt: e.Time}
if !e.StartedAt.IsZero() && !e.Time.Before(e.StartedAt) {
duration := e.Time.Sub(e.StartedAt)
v.Timing = compactDuration(duration)
v.DurationSeconds = strconv.FormatInt(int64(duration/time.Second), 10)
v.Started, v.StartedHuman = timestamp(e.StartedAt), humanTimestamp(e.StartedAt)
} else {
v.Timing, v.DurationSeconds, v.Started, v.StartedHuman = "—", "—", "—", "—"
}
v.Search = strings.ToLower(v.Repo + " " + v.Job + " " + v.Subject)
v.Meta, v.When = rowMeta(v.Trigger, v.Ref), rowWhen(v.Status, v.Timing, v.Age)
return v
}
func pendingView(now time.Time, p inbox.Pending) runView {
repo, job, rev, ref, kind := triggerView(p.Event)
at := submitted(p.ID)
v := runView{RunID: p.ID, Pronounce: pronounces(p.ID), ChildID: "run", Repo: repo, Job: job, Status: p.Phase, Ref: shortRef(ref), FullRef: valueOrDash(ref), Rev: shortRev(rev), FullRev: valueOrDash(rev), Trigger: kind, Timing: elapsed(now, at), DurationSeconds: durationSeconds(now, at), Age: elapsed(now, at), Started: timestamp(at), StartedHuman: humanTimestamp(at), SortTime: at}
v.Search = strings.ToLower(repo + " " + job)
v.Meta, v.When = rowMeta(v.Trigger, v.Ref), rowWhen(v.Status, v.Timing, v.Age)
return v
}
func runningParent(now time.Time, pending inbox.Pending, active runstate.Active) runView {
parent, child := pendingView(now, pending), activeView(now, active)
parent.Status, parent.Timing, parent.DurationSeconds, parent.Age, parent.Started, parent.StartedHuman = "running", child.Timing, child.DurationSeconds, child.Age, child.Started, child.StartedHuman
parent.Ref, parent.FullRef, parent.Rev, parent.FullRev = child.Ref, child.FullRef, child.Rev, child.FullRev
parent.Search = strings.ToLower(parent.Repo + " " + parent.Job)
parent.Meta, parent.When = rowMeta(parent.Trigger, parent.Ref), rowWhen(parent.Status, parent.Timing, parent.Age)
return parent
}
func activeView(now time.Time, a runstate.Active) runView {
v := runView{RunID: a.RunID, Pronounce: pronounces(a.RunID), ChildID: a.ChildID, Repo: a.Repo, Job: a.Job, Status: "running", Ref: shortRef(a.Ref), FullRef: valueOrDash(a.Ref), Rev: shortRev(a.Rev), FullRev: valueOrDash(a.Rev), Trigger: valueOrDash(a.Trigger), Timing: elapsed(now, a.StartedAt), DurationSeconds: durationSeconds(now, a.StartedAt), Age: elapsed(now, a.StartedAt), Started: timestamp(a.StartedAt), StartedHuman: humanTimestamp(a.StartedAt), SortTime: a.StartedAt}
v.Search = strings.ToLower(v.Repo + " " + v.Job)
v.Meta, v.When = rowMeta(v.Trigger, v.Ref), rowWhen(v.Status, v.Timing, v.Age)
return v
}
func triggerView(event trigger.Event) (repo, job, rev, ref, kind string) {
switch e := event.(type) {
case trigger.ManualEvent:
repo, job, kind = e.Repo, e.Job, "manual"
if e.Rev != nil {
rev = *e.Rev
}
if e.Ref != nil {
ref = *e.Ref
}
case trigger.PushEvent:
repo, job, rev, ref, kind = e.Repo, "push", e.New, e.Ref, "push"
case trigger.ScheduleEvent:
repo, job, rev, ref, kind = e.Repo, e.Job, e.Rev, e.Ref, "schedule"
}
return
}
func limit(v []runView, n int) []runView {
if len(v) > n {
return v[:n]
}
return v
}
func shortRev(v string) string {
if v == "" {
return "—"
}
if len(v) > 7 {
return v[:7]
}
return v
}
func shortRef(v string) string {
v = strings.TrimPrefix(v, "refs/heads/")
v = strings.TrimPrefix(v, "refs/tags/")
return valueOrDash(v)
}
func valueOrDash(v string) string {
if v == "" {
return "—"
}
return v
}
func compactDuration(v time.Duration) string {
if v < 0 {
return "—"
}
switch {
case v >= 24*time.Hour:
return fmt.Sprintf("%dd", v/(24*time.Hour))
case v >= time.Hour:
return fmt.Sprintf("%dh", v/time.Hour)
case v >= time.Minute:
return fmt.Sprintf("%dm", v/time.Minute)
default:
return fmt.Sprintf("%ds", v/time.Second)
}
}
func elapsed(now, then time.Time) string {
if then.IsZero() || now.Before(then) {
return "—"
}
return compactDuration(now.Sub(then))
}
func age(now, then time.Time) string {
value := elapsed(now, then)
if value == "—" {
return value
}
return value + " ago"
}
func rowMeta(trigger, ref string) string {
if trigger == "unknown" {
trigger = ""
}
if ref == "—" {
ref = ""
}
if trigger == "" {
return ref
}
if ref == "" {
return trigger
}
return trigger + " · " + ref
}
func rowWhen(status, timing, age string) string {
if status == "queued" || status == "preparing" || status == "running" {
return timing
}
if timing == "—" {
return age
}
if age == "—" {
return timing
}
return timing + " · " + age
}
func triggerDisplay(value string) string {
if value == "" {
return "unknown"
}
return value
}
func submitted(id string) time.Time {
n, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return time.Time{}
}
return time.Unix(0, n).UTC()
}
func timestamp(v time.Time) string {
if v.IsZero() {
return "—"
}
return v.UTC().Format(time.RFC3339)
}
func humanTimestamp(v time.Time) string {
if v.IsZero() {
return "—"
}
return v.UTC().Format("2 Jan 2006, 15:04 UTC")
}
func durationSeconds(now, then time.Time) string {
if then.IsZero() || now.Before(then) {
return "—"
}
return strconv.FormatInt(int64(now.Sub(then)/time.Second), 10)
}
func matrixDisplay(matrix map[string]string) string {
if len(matrix) == 0 {
return ""
}
keys := make([]string, 0, len(matrix))
for key := range matrix {
keys = append(keys, key)
}
sort.Strings(keys)
values := make([]string, 0, len(keys))
for _, key := range keys {
values = append(values, key+"="+matrix[key])
}
return strings.Join(values, " · ")
}