internal/control/dashboard.go

160fc0ec2bbbb4367ce5db1814c1a7404b656048
gitbay/internal/control/dashboard.go history · blame · raw

170 lines · 5321 bytes

  1package control
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"io"
  7	"strconv"
  8
  9	"gitbay.org/gitbay/internal/gitutil"
 10	"gitbay.org/gitbay/internal/policy"
 11	"gitbay.org/gitbay/internal/protocol"
 12	"gitbay.org/gitbay/internal/store"
 13)
 14
 15func init() {
 16	register(Command{Path: []string{"dashboard"},
 17		Summary:  "one read for the account dashboard: pinned repos, open MRs, assigned issues, recent builds",
 18		ReadOnly: true, Run: runDashboard})
 19	register(Command{Path: []string{"feed"},
 20		Summary:  "activity on repositories you can reach: feed [--limit <n>] [--cursor <c>]",
 21		ReadOnly: true, Run: runFeed})
 22}
 23
 24// dashboardItem is one open issue or MR row, with its repo resolved so a
 25// client renders the aggregate without further reads.
 26type dashboardItem struct {
 27	Repo      string `json:"repo"`
 28	Number    int64  `json:"number"`
 29	Title     string `json:"title"`
 30	Author    string `json:"author"`
 31	State     string `json:"state"`
 32	UpdatedAt string `json:"updated_at"`
 33}
 34
 35func runDashboard(c *Ctx, args []string) int {
 36	if len(args) != 0 {
 37		return c.fail(protocol.ExitUsage, "usage: dashboard")
 38	}
 39	type pinnedOut struct {
 40		Path        string `json:"path"`
 41		Visibility  string `json:"visibility"`
 42		Description string `json:"description,omitempty"`
 43		Archived    bool   `json:"archived,omitempty"`
 44	}
 45	type buildOut struct {
 46		Repo       string `json:"repo"`
 47		Number     int64  `json:"number"`
 48		Job        string `json:"job"`
 49		Status     string `json:"status"`
 50		SHA        string `json:"sha"`
 51		Ref        string `json:"ref"`
 52		CreatedAt  string `json:"created_at"`
 53		FinishedAt string `json:"finished_at,omitempty"`
 54	}
 55	type out struct {
 56		Pinned   []pinnedOut     `json:"pinned"`
 57		MRs      []dashboardItem `json:"open_mrs"`
 58		Assigned []dashboardItem `json:"assigned_issues"`
 59		Builds   []buildOut      `json:"builds"`
 60	}
 61	d := out{Pinned: []pinnedOut{}, MRs: []dashboardItem{}, Assigned: []dashboardItem{}, Builds: []buildOut{}}
 62
 63	pinned, err := c.Store.PinnedRepos(c.User.ID)
 64	if err != nil {
 65		return c.fail(protocol.ExitFailure, "%v", err)
 66	}
 67	for _, r := range pinned {
 68		grant, err := c.Store.AccessRole(r.ID, c.User.ID)
 69		if err != nil {
 70			return c.fail(protocol.ExitFailure, "%v", err)
 71		}
 72		if !policy.CanRead(c.User, r, grant) {
 73			continue
 74		}
 75		desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, r.OwnerName, r.Name))
 76		d.Pinned = append(d.Pinned, pinnedOut{r.Path(), r.Visibility, desc, r.Settings.Archived})
 77	}
 78
 79	mrs, err := c.Store.DashboardMRs(c.User.ID)
 80	if err != nil {
 81		return c.fail(protocol.ExitFailure, "%v", err)
 82	}
 83	for _, m := range mrs {
 84		d.MRs = append(d.MRs, dashboardItem{m.RepoPath, m.Number, m.Title, m.Author, m.State, m.UpdatedAt})
 85	}
 86
 87	assigned, err := c.Store.AssignedIssues(c.User.ID)
 88	if err != nil {
 89		return c.fail(protocol.ExitFailure, "%v", err)
 90	}
 91	for _, i := range assigned {
 92		d.Assigned = append(d.Assigned, dashboardItem{i.RepoPath, i.Number, i.Title, i.Author, i.State, i.UpdatedAt})
 93	}
 94
 95	builds, err := c.Store.RecentBuilds(c.User.ID, 20)
 96	if err != nil {
 97		return c.fail(protocol.ExitFailure, "%v", err)
 98	}
 99	for _, b := range builds {
100		d.Builds = append(d.Builds, buildOut{b.RepoPath, b.Number, b.Job, b.Status, b.SHA, b.Ref, b.CreatedAt, b.FinishedAt})
101	}
102
103	return c.emit(d, func(w io.Writer) {
104		fmt.Fprintln(w, "pinned:")
105		for _, p := range d.Pinned {
106			mark := ""
107			if p.Archived {
108				mark = "\t[archived]"
109			}
110			fmt.Fprintf(w, "  %s\t%s\t%s%s\n", p.Path, p.Visibility, p.Description, mark)
111		}
112		fmt.Fprintln(w, "open merge requests:")
113		for _, m := range d.MRs {
114			fmt.Fprintf(w, "  %s!%d\t%s\t%s\n", m.Repo, m.Number, m.Title, m.Author)
115		}
116		fmt.Fprintln(w, "assigned issues:")
117		for _, i := range d.Assigned {
118			fmt.Fprintf(w, "  %s#%d\t%s\t%s\n", i.Repo, i.Number, i.Title, i.Author)
119		}
120		fmt.Fprintln(w, "builds:")
121		for _, b := range d.Builds {
122			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)
123		}
124	})
125}
126
127// feedDefaultLimit caps a bare `feed` call; pagination reaches further
128// back.
129const feedDefaultLimit = 50
130
131func runFeed(c *Ctx, args []string) int {
132	rest, p, code := parsePageFlags(c, args, "feed", true)
133	if code >= 0 {
134		return code
135	}
136	if len(rest) != 0 {
137		return c.fail(protocol.ExitUsage, "usage: feed [--limit <n>] [--cursor <c>]")
138	}
139	if p.limit == 0 {
140		p.limit = feedDefaultLimit
141	}
142	events, err := c.Store.RecentEvents(c.User.ID, p.queryLimit(), p.keyInt())
143	if err != nil {
144		return c.fail(protocol.ExitFailure, "%v", err)
145	}
146	events, next := trimPage(p, events, "feed", func(e store.FeedEvent) string {
147		return strconv.FormatInt(e.ID, 10)
148	})
149	type feedOut struct {
150		ID        int64           `json:"id"`
151		Repo      string          `json:"repo"`
152		Actor     string          `json:"actor,omitempty"`
153		Kind      string          `json:"kind"`
154		Data      json.RawMessage `json:"data,omitempty"`
155		CreatedAt string          `json:"created_at"`
156	}
157	var ds []feedOut
158	for _, e := range events {
159		d := feedOut{ID: e.ID, Repo: e.RepoPath, Actor: e.Actor, Kind: e.Kind, CreatedAt: e.CreatedAt}
160		if json.Valid([]byte(e.Data)) {
161			d.Data = json.RawMessage(e.Data)
162		}
163		ds = append(ds, d)
164	}
165	return c.emitPage(p, ds, next, func(w io.Writer) {
166		for _, d := range ds {
167			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", d.CreatedAt, d.Actor, d.Kind, d.Repo, string(d.Data))
168		}
169	})
170}