Skip to content

Commit 6e259a7

Browse files
feat: Pull Request Reviews (#402)
Closes #70 EDIT: realizing that #70 is already covered by current features This introduces a new query type of Pull Request Reviews. This is separate from Pull Requests due to adding more paginated queries to handle more than 100 reviews (I imagine this is quite rare) as well as pulling more data. Combining pull requests and reviews results in a lot of duplication that doesn't make sense for other PR use cases. I believe I've implemented all the necessary components here for this integration, but please let me know if I'm missing anything ## Use Case - Supports the use case documented in #70 - Supports creating a page like Panda's Leaderboard page --------- Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
1 parent 179df01 commit 6e259a7

12 files changed

+816
-0
lines changed

.changeset/stupid-mangos-cry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'grafana-github-datasource': minor
3+
---
4+
5+
Adds support for Pull Request Review queries

pkg/github/datasource.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ func (d *Datasource) HandlePullRequestsQuery(ctx context.Context, query *models.
8989
return GetPullRequestsInRange(ctx, d.client, opt, req.TimeRange.From, req.TimeRange.To)
9090
}
9191

92+
// HandleReviewsQuery is the query handler for listing GitHub Pull Request Reviews
93+
func (d *Datasource) HandleReviewsQuery(ctx context.Context, query *models.PullRequestsQuery, req backend.DataQuery) (dfutil.Framer, error) {
94+
opt := models.PullRequestOptionsWithRepo(query.Options, query.Owner, query.Repository)
95+
96+
if req.TimeRange.From.Unix() <= 0 && req.TimeRange.To.Unix() <= 0 {
97+
return GetAllPullRequestReviews(ctx, d.client, opt)
98+
}
99+
return GetPullRequestReviewsInRange(ctx, d.client, opt, req.TimeRange.From, req.TimeRange.To)
100+
}
101+
92102
// HandleContributorsQuery is the query handler for listing GitHub Contributors
93103
func (d *Datasource) HandleContributorsQuery(ctx context.Context, query *models.ContributorsQuery, req backend.DataQuery) (dfutil.Framer, error) {
94104
opt := models.ListContributorsOptions{

pkg/github/pull_request_reviews.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/grafana/github-datasource/pkg/models"
9+
"github.com/grafana/grafana-plugin-sdk-go/data"
10+
"github.com/pkg/errors"
11+
"github.com/shurcooL/githubv4"
12+
)
13+
14+
// QueryListPullRequestReviews lists all pull request reviews in a repository
15+
//
16+
// {
17+
// search(query: "is:pr repo:grafana/grafana merged:2020-08-19..*", type: ISSUE, first: 100) {
18+
// nodes {
19+
// ... on PullRequest {
20+
// Number
21+
// Title
22+
// URL
23+
// State
24+
// Author
25+
// Repository
26+
// reviews(first: 100) {
27+
// createdAt
28+
// updatedAt
29+
// state
30+
// url
31+
// author {
32+
// id
33+
// login
34+
// name
35+
// company
36+
// email
37+
// url
38+
// }
39+
// comments(first: 0) {
40+
// totalCount
41+
// }
42+
// }
43+
// }
44+
// }
45+
// }
46+
// }
47+
type QueryListPullRequestReviews struct {
48+
Search struct {
49+
Nodes []struct {
50+
PullRequest struct {
51+
Number int64
52+
Title string
53+
URL string
54+
State githubv4.PullRequestState
55+
Author Author
56+
Repository Repository
57+
Reviews struct {
58+
Nodes []struct {
59+
Review struct {
60+
CreatedAt githubv4.DateTime
61+
UpdatedAt githubv4.DateTime
62+
URL string
63+
Author Author
64+
State githubv4.PullRequestReviewState
65+
Comments struct {
66+
TotalCount int64
67+
} `graphql:"comments(first: 0)"`
68+
} `graphql:"... on PullRequestReview"`
69+
}
70+
PageInfo models.PageInfo
71+
} `graphql:"reviews(first: 100, after: $reviewCursor)"`
72+
} `graphql:"... on PullRequest"`
73+
}
74+
PageInfo models.PageInfo
75+
} `graphql:"search(query: $query, type: ISSUE, first: 100, after: $prCursor)"`
76+
}
77+
78+
type Author struct {
79+
User models.User `graphql:"... on User"`
80+
}
81+
82+
type Review struct {
83+
CreatedAt githubv4.DateTime
84+
UpdatedAt githubv4.DateTime
85+
URL string
86+
Author Author
87+
State githubv4.PullRequestReviewState
88+
CommentsCount int64
89+
}
90+
91+
type PullRequestWithReviews struct {
92+
Number int64
93+
Title string
94+
State githubv4.PullRequestState
95+
URL string
96+
Author Author
97+
Repository Repository
98+
Reviews []Review
99+
}
100+
101+
// PullRequestReviews is a list of GitHub Pull Request Reviews
102+
type PullRequestReviews []PullRequestWithReviews
103+
104+
// Frames coverts the list of Pull Request Reviews to a Grafana DataFrame
105+
func (prs PullRequestReviews) Frames() data.Frames {
106+
frame := data.NewFrame(
107+
"pull_request_reviews",
108+
data.NewField("pull_request_number", nil, []int64{}),
109+
data.NewField("pull_request_title", nil, []string{}),
110+
data.NewField("pull_request_state", nil, []string{}),
111+
data.NewField("pull_request_url", nil, []string{}),
112+
data.NewField("pull_request_author_name", nil, []string{}),
113+
data.NewField("pull_request_author_login", nil, []string{}),
114+
data.NewField("pull_request_author_email", nil, []string{}),
115+
data.NewField("pull_request_author_company", nil, []string{}),
116+
data.NewField("repository", nil, []string{}),
117+
data.NewField("review_author_name", nil, []string{}),
118+
data.NewField("review_author_login", nil, []string{}),
119+
data.NewField("review_author_email", nil, []string{}),
120+
data.NewField("review_author_company", nil, []string{}),
121+
data.NewField("review_url", nil, []string{}),
122+
data.NewField("review_state", nil, []string{}),
123+
data.NewField("review_comment_count", nil, []int64{}),
124+
data.NewField("review_updated_at", nil, []time.Time{}),
125+
data.NewField("review_created_at", nil, []time.Time{}),
126+
)
127+
128+
for _, pr := range prs {
129+
for _, review := range pr.Reviews {
130+
frame.AppendRow(
131+
pr.Number,
132+
pr.Title,
133+
string(pr.State),
134+
pr.URL,
135+
pr.Author.User.Name,
136+
pr.Author.User.Login,
137+
pr.Author.User.Email,
138+
pr.Author.User.Company,
139+
pr.Repository.NameWithOwner,
140+
review.Author.User.Name,
141+
review.Author.User.Login,
142+
review.Author.User.Email,
143+
review.Author.User.Company,
144+
review.URL,
145+
string(review.State),
146+
review.CommentsCount,
147+
review.UpdatedAt.Time,
148+
review.CreatedAt.Time,
149+
)
150+
}
151+
}
152+
153+
return data.Frames{frame}
154+
}
155+
156+
// GetAllPullRequestReviews uses the graphql search endpoint API to search all pull requests in the repository
157+
// and all reviews for those pull requests.
158+
func GetAllPullRequestReviews(ctx context.Context, client models.Client, opts models.ListPullRequestsOptions) (PullRequestReviews, error) {
159+
var (
160+
variables = map[string]interface{}{
161+
"prCursor": (*githubv4.String)(nil),
162+
"reviewCursor": (*githubv4.String)(nil),
163+
"query": githubv4.String(buildQuery(opts)),
164+
}
165+
166+
pullRequestReviews = PullRequestReviews{}
167+
)
168+
169+
for {
170+
q := &QueryListPullRequestReviews{}
171+
if err := client.Query(ctx, q, variables); err != nil {
172+
return nil, errors.WithStack(err)
173+
}
174+
175+
prs := make([]PullRequestWithReviews, len(q.Search.Nodes))
176+
177+
for i, prNode := range q.Search.Nodes {
178+
pr := prNode.PullRequest
179+
180+
prs[i] = PullRequestWithReviews{
181+
Number: pr.Number,
182+
Title: pr.Title,
183+
State: pr.State,
184+
URL: pr.URL,
185+
Author: pr.Author,
186+
Repository: pr.Repository,
187+
}
188+
189+
for {
190+
for _, reviewNode := range pr.Reviews.Nodes {
191+
review := reviewNode.Review
192+
193+
prs[i].Reviews = append(prs[i].Reviews, Review{
194+
CreatedAt: review.CreatedAt,
195+
UpdatedAt: review.UpdatedAt,
196+
URL: review.URL,
197+
Author: review.Author,
198+
State: review.State,
199+
CommentsCount: review.Comments.TotalCount,
200+
})
201+
}
202+
203+
if !pr.Reviews.PageInfo.HasNextPage {
204+
variables["reviewCursor"] = (*githubv4.String)(nil)
205+
break
206+
}
207+
208+
variables["reviewCursor"] = pr.Reviews.PageInfo.EndCursor
209+
if err := client.Query(ctx, q, variables); err != nil {
210+
return nil, errors.WithStack(err)
211+
}
212+
}
213+
}
214+
215+
pullRequestReviews = append(pullRequestReviews, prs...)
216+
217+
if !q.Search.PageInfo.HasNextPage {
218+
break
219+
}
220+
variables["prCursor"] = q.Search.PageInfo.EndCursor
221+
}
222+
223+
return pullRequestReviews, nil
224+
}
225+
226+
// GetPullRequestReviewsInRange uses the graphql search endpoint API to find pull request reviews in the given time range.
227+
func GetPullRequestReviewsInRange(ctx context.Context, client models.Client, opts models.ListPullRequestsOptions, from time.Time, to time.Time) (PullRequestReviews, error) {
228+
var q string
229+
230+
if opts.TimeField != models.PullRequestNone {
231+
q = fmt.Sprintf("%s:%s..%s", opts.TimeField.String(), from.Format(time.RFC3339), to.Format(time.RFC3339))
232+
}
233+
234+
if opts.Query != nil {
235+
q = fmt.Sprintf("%s %s", *opts.Query, q)
236+
}
237+
238+
return GetAllPullRequestReviews(ctx, client, models.ListPullRequestsOptions{
239+
Repository: opts.Repository,
240+
Owner: opts.Owner,
241+
TimeField: opts.TimeField,
242+
Query: &q,
243+
})
244+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
"github.com/grafana/github-datasource/pkg/dfutil"
7+
"github.com/grafana/github-datasource/pkg/models"
8+
"github.com/grafana/grafana-plugin-sdk-go/backend"
9+
)
10+
11+
func (s *QueryHandler) handlePullRequestReviewsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
12+
query := &models.PullRequestsQuery{}
13+
if err := UnmarshalQuery(q.JSON, query); err != nil {
14+
return *err
15+
}
16+
return dfutil.FrameResponseWithError(s.Datasource.HandleReviewsQuery(ctx, query, q))
17+
}
18+
19+
// HandlePullRequestReviews handles the plugin query for github Pull Request Reviews
20+
func (s *QueryHandler) HandlePullRequestReviews(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
21+
return &backend.QueryDataResponse{
22+
Responses: processQueries(ctx, req, s.handlePullRequestReviewsQuery),
23+
}, nil
24+
}

0 commit comments

Comments
 (0)