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