internal/control/dashboard.go

93b38a063b3b4b161a1baf976e92f53d02b1a221
gitbay/internal/control/dashboard.go history · blame · raw

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