internal/control/dashboard.go
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}