internal/control/dashboard.go

ddab52165310177c117b89a07d5b155e56fb56dc
gitbay/internal/control/dashboard.go history · blame · raw

261 lines · 8789 bytes

  1package control
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"io"
  7	"strconv"
  8
  9	"gitbay.org/gitbay/internal/buildinfo"
 10	"gitbay.org/gitbay/internal/gitutil"
 11	"gitbay.org/gitbay/internal/policy"
 12	"gitbay.org/gitbay/internal/protocol"
 13	"gitbay.org/gitbay/internal/store"
 14)
 15
 16func init() {
 17	register(Command{Path: []string{"dashboard"},
 18		Summary:  "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
 19		Usage:    "dashboard",
 20		ReadOnly: true, Run: runDashboard})
 21	register(Command{Path: []string{"feed"},
 22		Summary:  "activity on repositories you can reach",
 23		Usage:    "feed [--limit <n>] [--cursor <c>]",
 24		ReadOnly: true, Run: runFeed})
 25}
 26
 27// dashboardItem is one open issue or MR row, with its repo resolved so a
 28// client renders the aggregate without further reads.
 29type dashboardItem struct {
 30	Repo      string `json:"repo"`
 31	Number    int64  `json:"number"`
 32	Title     string `json:"title"`
 33	Author    string `json:"author"`
 34	State     string `json:"state"`
 35	UpdatedAt string `json:"updated_at"`
 36}
 37
 38func runDashboard(c *Ctx, args []string) int {
 39	if len(args) != 0 {
 40		return c.fail(protocol.ExitUsage, "usage: dashboard")
 41	}
 42	type pinnedOut struct {
 43		Path        string `json:"path"`
 44		Visibility  string `json:"visibility"`
 45		Description string `json:"description,omitempty"`
 46		Archived    bool   `json:"archived,omitempty"`
 47	}
 48	type buildOut struct {
 49		Repo       string `json:"repo"`
 50		Number     int64  `json:"number"`
 51		Job        string `json:"job"`
 52		Status     string `json:"status"`
 53		SHA        string `json:"sha"`
 54		Ref        string `json:"ref"`
 55		CreatedAt  string `json:"created_at"`
 56		FinishedAt string `json:"finished_at,omitempty"`
 57	}
 58	// serverOut is admin-only. The exact build a host is running narrows down
 59	// which known issues apply to it, so it is not everyone's to read; the
 60	// person who needs it is the operator.
 61	type serverOut struct {
 62		Commit string `json:"commit"`
 63	}
 64	type out struct {
 65		Reviews  []dashboardItem `json:"review_queue"`
 66		Assigned []dashboardItem `json:"assigned_issues"`
 67		MRs      []dashboardItem `json:"open_mrs"`
 68		Issues   []dashboardItem `json:"open_issues"`
 69		Pinned   []pinnedOut     `json:"pinned"`
 70		Activity []feedOut       `json:"recent_activity"`
 71		Builds   []buildOut      `json:"builds"`
 72		Server   *serverOut      `json:"server,omitempty"`
 73		// Queues is admin-only: every background worker's backlog and
 74		// failures, the operator's view of what is stuck.
 75		Queues *store.Queues `json:"queues,omitempty"`
 76	}
 77	d := out{
 78		Reviews: []dashboardItem{}, Assigned: []dashboardItem{}, MRs: []dashboardItem{},
 79		Issues: []dashboardItem{}, Pinned: []pinnedOut{}, Activity: []feedOut{}, Builds: []buildOut{},
 80	}
 81
 82	pinned, err := c.Store.PinnedRepos(c.User.ID)
 83	if err != nil {
 84		return c.fail(protocol.ExitFailure, "%v", err)
 85	}
 86	for _, r := range pinned {
 87		grant, err := c.Store.AccessRole(r.ID, c.User.ID)
 88		if err != nil {
 89			return c.fail(protocol.ExitFailure, "%v", err)
 90		}
 91		if !policy.CanRead(c.User, r, grant) {
 92			continue
 93		}
 94		desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, r.OwnerName, r.Name))
 95		d.Pinned = append(d.Pinned, pinnedOut{r.Path(), r.Visibility, desc, r.Settings.Archived})
 96	}
 97
 98	mrs, err := c.Store.DashboardMRs(c.User.ID)
 99	if err != nil {
100		return c.fail(protocol.ExitFailure, "%v", err)
101	}
102	for _, m := range mrs {
103		d.MRs = append(d.MRs, dashboardItem{m.RepoPath, m.Number, m.Title, m.Author, m.State, m.UpdatedAt})
104	}
105
106	reviews, err := c.Store.ReviewQueue(c.User.ID)
107	if err != nil {
108		return c.fail(protocol.ExitFailure, "%v", err)
109	}
110	for _, m := range reviews {
111		d.Reviews = append(d.Reviews, dashboardItem{m.RepoPath, m.Number, m.Title, m.Author, m.State, m.UpdatedAt})
112	}
113
114	assigned, err := c.Store.AssignedIssues(c.User.ID)
115	if err != nil {
116		return c.fail(protocol.ExitFailure, "%v", err)
117	}
118	for _, i := range assigned {
119		d.Assigned = append(d.Assigned, dashboardItem{i.RepoPath, i.Number, i.Title, i.Author, i.State, i.UpdatedAt})
120	}
121
122	issues, err := c.Store.DashboardIssues(c.User.ID)
123	if err != nil {
124		return c.fail(protocol.ExitFailure, "%v", err)
125	}
126	for _, i := range issues {
127		d.Issues = append(d.Issues, dashboardItem{i.RepoPath, i.Number, i.Title, i.Author, i.State, i.UpdatedAt})
128	}
129
130	events, err := c.Store.RecentEvents(c.User.ID, 20, 0)
131	if err != nil {
132		return c.fail(protocol.ExitFailure, "%v", err)
133	}
134	d.Activity = feedOutputs(events)
135
136	builds, err := c.Store.RecentBuilds(c.User.ID, 20)
137	if err != nil {
138		return c.fail(protocol.ExitFailure, "%v", err)
139	}
140	for _, b := range builds {
141		d.Builds = append(d.Builds, buildOut{b.RepoPath, b.Number, b.Job, b.Status, b.SHA, b.Ref, b.CreatedAt, b.FinishedAt})
142	}
143	if c.User.IsAdmin {
144		d.Server = &serverOut{Commit: buildinfo.String()}
145		q, err := c.Store.QueueStatus()
146		if err != nil {
147			return c.fail(protocol.ExitFailure, "%v", err)
148		}
149		d.Queues = &q
150	}
151
152	return c.emit(d, func(w io.Writer) {
153		fmt.Fprintln(w, "waiting on your review:")
154		printDashboardItems(w, d.Reviews, "!")
155		fmt.Fprintln(w, "assigned to you:")
156		printDashboardItems(w, d.Assigned, "#")
157		fmt.Fprintln(w, "open merge requests:")
158		printDashboardItems(w, d.MRs, "!")
159		fmt.Fprintln(w, "open issues:")
160		printDashboardItems(w, d.Issues, "#")
161		fmt.Fprintln(w, "pinned:")
162		for _, p := range d.Pinned {
163			mark := ""
164			if p.Archived {
165				mark = "\t[archived]"
166			}
167			fmt.Fprintf(w, "  %s\t%s\t%s%s\n", p.Path, p.Visibility, p.Description, mark)
168		}
169		fmt.Fprintln(w, "recent activity:")
170		for _, e := range d.Activity {
171			fmt.Fprintf(w, "  %s\t%s\t%s\t%s\t%s\n", e.CreatedAt, e.Actor, e.Kind, e.Repo, string(e.Data))
172		}
173		fmt.Fprintln(w, "builds:")
174		for _, b := range d.Builds {
175			fmt.Fprintf(w, "  %s\t%d\t%s\t%s\t%.10s\t%s\n", b.Repo, b.Number, b.Job, b.Status, b.SHA, b.Ref)
176		}
177		if d.Server != nil {
178			fmt.Fprintf(w, "server:\n  build %s\n", d.Server.Commit)
179		}
180		if q := d.Queues; q != nil {
181			fmt.Fprintln(w, "queues:")
182			fmt.Fprintf(w, "  webhooks\tpending %d\tretrying %d\tfailed %d\n", q.Webhooks.Pending, q.Webhooks.Retrying, q.Webhooks.Failed)
183			for _, it := range q.Webhooks.Items {
184				fmt.Fprintf(w, "    %s\t%s\tattempts %d\t%s\n", it.Repo, it.URL, it.Attempts, it.LastError)
185			}
186			fmt.Fprintf(w, "  mail\tpending %d\tretrying %d\tfailed %d\n", q.Mail.Pending, q.Mail.Retrying, q.Mail.Failed)
187			for _, it := range q.Mail.Items {
188				fmt.Fprintf(w, "    %s\t%s\tattempts %d\t%s\n", it.Recipient, it.Subject, it.Attempts, it.LastError)
189			}
190			fmt.Fprintf(w, "  mirrors\tdirty %d\terrors %d\n", q.Mirrors.Dirty, q.Mirrors.Errors)
191			for _, it := range q.Mirrors.Items {
192				fmt.Fprintf(w, "    %s\t%s\t%s\t%s\n", it.Repo, it.Direction, it.URL, it.LastError)
193			}
194			fmt.Fprintf(w, "  builds\tpending %d\trunning %d\n", q.Builds.Pending, q.Builds.Running)
195			for _, it := range q.Builds.Items {
196				fmt.Fprintf(w, "    %s\t%d\t%s\tsince %s\n", it.Repo, it.Number, it.Job, it.StartedAt)
197			}
198			fmt.Fprintf(w, "  deps\terrors %d\n", q.Deps.Errors)
199			for _, it := range q.Deps.Items {
200				fmt.Fprintf(w, "    %s\t%s\n", it.Repo, it.LastError)
201			}
202		}
203	})
204}
205
206func printDashboardItems(w io.Writer, items []dashboardItem, marker string) {
207	for _, item := range items {
208		fmt.Fprintf(w, "  %s%s%d\t%s\t%s\n", item.Repo, marker, item.Number, item.Title, item.Author)
209	}
210}
211
212// feedDefaultLimit caps a bare `feed` call; pagination reaches further
213// back.
214const feedDefaultLimit = 50
215
216type feedOut struct {
217	ID        int64           `json:"id"`
218	Repo      string          `json:"repo"`
219	Actor     string          `json:"actor,omitempty"`
220	Kind      string          `json:"kind"`
221	Data      json.RawMessage `json:"data,omitempty"`
222	CreatedAt string          `json:"created_at"`
223}
224
225func feedOutputs(events []store.FeedEvent) []feedOut {
226	ds := make([]feedOut, 0, len(events))
227	for _, e := range events {
228		d := feedOut{ID: e.ID, Repo: e.RepoPath, Actor: e.Actor, Kind: e.Kind, CreatedAt: e.CreatedAt}
229		if json.Valid([]byte(e.Data)) {
230			d.Data = json.RawMessage(e.Data)
231		}
232		ds = append(ds, d)
233	}
234	return ds
235}
236
237func runFeed(c *Ctx, args []string) int {
238	rest, p, code := parsePageFlags(c, args, "feed", true)
239	if code >= 0 {
240		return code
241	}
242	if len(rest) != 0 {
243		return c.fail(protocol.ExitUsage, "usage: feed [--limit <n>] [--cursor <c>]")
244	}
245	if p.limit == 0 {
246		p.limit = feedDefaultLimit
247	}
248	events, err := c.Store.RecentEvents(c.User.ID, p.queryLimit(), p.keyInt())
249	if err != nil {
250		return c.fail(protocol.ExitFailure, "%v", err)
251	}
252	events, next := trimPage(p, events, "feed", func(e store.FeedEvent) string {
253		return strconv.FormatInt(e.ID, 10)
254	})
255	ds := feedOutputs(events)
256	return c.emitPage(p, ds, next, func(w io.Writer) {
257		for _, d := range ds {
258			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", d.CreatedAt, d.Actor, d.Kind, d.Repo, string(d.Data))
259		}
260	})
261}