Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions pkg/github/branches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package github

import (
"context"
"time"

"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/shurcooL/githubv4"
)

type branchDTO struct {
Name string
CommitSHA string
AuthorName string
AuthorLogin string
CommitDate time.Time
}

// Branches is a list of GitHub branches
type Branches []branchDTO

// Frames converts the list of branches to a Grafana DataFrame
func (b Branches) Frames() data.Frames {
frame := data.NewFrame(
"branches",
data.NewField("name", nil, []string{}),
data.NewField("commit_sha", nil, []string{}),
data.NewField("author", nil, []string{}),
data.NewField("author_login", nil, []string{}),
data.NewField("commit_date", nil, []time.Time{}),
)

for _, v := range b {
frame.AppendRow(
v.Name,
v.CommitSHA,
v.AuthorName,
v.AuthorLogin,
v.CommitDate,
)
}

return data.Frames{frame}
}

// QueryListBranches is the GraphQL query for listing GitHub branches in a repository
//
// {
// repository(name: "grafana", owner: "grafana") {
// refs(refPrefix: "refs/heads/", first: 100, after: $cursor, query: $query) {
// nodes {
// name
// target {
// ... on Commit {
// oid
// author {
// date
// user {
// login
// name
// }
// }
// }
// }
// }
// pageInfo {
// hasNextPage
// endCursor
// }
// }
// }
// }
type QueryListBranches struct {
Repository struct {
Refs struct {
Nodes []struct {
Name string
Target struct {
Commit commit `graphql:"... on Commit"`
}
}
PageInfo models.PageInfo
} `graphql:"refs(refPrefix: \"refs/heads/\", first: 100, after: $cursor, query: $query)"`
} `graphql:"repository(name: $name, owner: $owner)"`
}

// GetAllBranches retrieves every branch from a repository, filtered by the optional query string
func GetAllBranches(ctx context.Context, client models.Client, opts models.ListBranchesOptions) (Branches, error) {
var (
variables = map[string]interface{}{
"cursor": (*githubv4.String)(nil),
"owner": githubv4.String(opts.Owner),
"name": githubv4.String(opts.Repository),
"query": githubv4.String(opts.Query),
}

branches = []branchDTO{}
)

for {
q := &QueryListBranches{}
if err := client.Query(ctx, q, variables); err != nil {
return nil, err
}

for _, node := range q.Repository.Refs.Nodes {
branches = append(branches, branchDTO{
Name: node.Name,
CommitSHA: node.Target.Commit.OID,
AuthorName: node.Target.Commit.Author.User.Name,
AuthorLogin: node.Target.Commit.Author.User.Login,
CommitDate: node.Target.Commit.Author.Date.Time,
})
}

if !q.Repository.Refs.PageInfo.HasNextPage {
break
}
variables["cursor"] = q.Repository.Refs.PageInfo.EndCursor
}

return branches, nil
}
24 changes: 24 additions & 0 deletions pkg/github/branches_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package github

import (
"context"

"github.com/grafana/github-datasource/pkg/dfutil"
"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)

func (s *QueryHandler) handleBranchesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
query := &models.BranchesQuery{}
if err := UnmarshalQuery(q.JSON, query); err != nil {
return *err
}
return dfutil.FrameResponseWithError(s.Datasource.HandleBranchesQuery(ctx, query, q))
}

// HandleBranches handles the plugin query for github branches
func (s *QueryHandler) HandleBranches(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return &backend.QueryDataResponse{
Responses: processQueries(ctx, req, s.handleBranchesQuery),
}, nil
}
32 changes: 32 additions & 0 deletions pkg/github/branches_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package github

import (
"context"
"testing"

"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/github-datasource/pkg/testutil"
)

func TestListBranches(t *testing.T) {
var (
ctx = context.Background()
opts = models.ListBranchesOptions{
Repository: "grafana",
Owner: "grafana",
Query: "release/",
}
)

testVariables := testutil.GetTestVariablesFunction("query", "name", "owner", "cursor")

client := testutil.NewTestClient(t,
testVariables,
testutil.GetTestQueryFunction(&QueryListBranches{}),
)

_, err := GetAllBranches(ctx, client, opts)
if err != nil {
t.Fatal(err)
}
}
10 changes: 10 additions & 0 deletions pkg/github/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ func (d *Datasource) HandleTagsQuery(ctx context.Context, query *models.TagsQuer
return GetTagsInRange(ctx, d.client, opt, req.TimeRange.From, req.TimeRange.To)
}

// HandleBranchesQuery is the query handler for listing GitHub Branches
func (d *Datasource) HandleBranchesQuery(ctx context.Context, query *models.BranchesQuery, req backend.DataQuery) (dfutil.Framer, error) {
opt := models.ListBranchesOptions{
Repository: query.Repository,
Owner: query.Owner,
Query: query.Options.Query,
}
return GetAllBranches(ctx, d.client, opt)
}

// HandleReleasesQuery is the query handler for listing GitHub Releases
func (d *Datasource) HandleReleasesQuery(ctx context.Context, query *models.ReleasesQuery, req backend.DataQuery) (dfutil.Framer, error) {
opt := models.ListReleasesOptions{
Expand Down
1 change: 1 addition & 0 deletions pkg/github/query_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux {
register(models.QueryTypePullRequestReviews, s.HandlePullRequestReviews)
register(models.QueryTypeReleases, s.HandleReleases)
register(models.QueryTypeTags, s.HandleTags)
register(models.QueryTypeBranches, s.HandleBranches)
register(models.QueryTypePackages, s.HandlePackages)
register(models.QueryTypeMilestones, s.HandleMilestones)
register(models.QueryTypeRepositories, s.HandleRepositories)
Expand Down
13 changes: 13 additions & 0 deletions pkg/models/branches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

// ListBranchesOptions are the available options when listing branches
type ListBranchesOptions struct {
// Repository is the name of the repository being queried (ex: grafana)
Repository string `json:"repository"`

// Owner is the owner of the repository (ex: grafana)
Owner string `json:"owner"`

// Query filters branches by name prefix/substring (ex: release/)
Query string `json:"query"`
}
8 changes: 8 additions & 0 deletions pkg/models/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const (
QueryTypeCommitFiles QueryType = "Commit_Files"
// QueryTypePullRequestFiles is used when querying files changed in a specific pull request
QueryTypePullRequestFiles QueryType = "Pull_Request_Files"
// QueryTypeBranches is used when querying branches in a GitHub repository
QueryTypeBranches QueryType = "Branches"
)

// Query refers to the structure of a query built using the QueryEditor.
Expand Down Expand Up @@ -185,3 +187,9 @@ type PullRequestFilesQuery struct {
Query
Options PullRequestFilesOptions `json:"options"`
}

// BranchesQuery is used when querying for branches in a GitHub repository
type BranchesQuery struct {
Query
Options ListBranchesOptions `json:"options"`
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const QueryTypes = [
'Issues',
'Contributors',
'Tags',
'Branches',
'Releases',
'Pull_Requests',
'Pull_Request_Reviews',
Expand Down
10 changes: 9 additions & 1 deletion src/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export type TagsOptions = Options & {}
type TagsQuery = BaseQuery<'Tags', TagsOptions>
//#endregion

//#region Branches Query
export type BranchesOptions = Options & {
query?: string;
}
type BranchesQuery = BaseQuery<'Branches', BranchesOptions>
//#endregion

//#region Releases Query
export type ReleasesOptions = Options & {}
type ReleasesQuery = BaseQuery<'Releases', ReleasesOptions>
Expand Down Expand Up @@ -194,7 +201,8 @@ export type GitHubQuery =
Workflow_RunsQuery |
Workflow_UsageQuery |
WorkflowsQuery |
DeploymentsQuery
DeploymentsQuery |
BranchesQuery

export type GitHubVariableQuery = { key?: string; field?: string; } & GitHubQuery

Expand Down
6 changes: 6 additions & 0 deletions src/views/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { QueryEditorWorkflowUsage } from './QueryEditorWorkflowUsage';
import { QueryEditorWorkflowRuns } from './QueryEditorWorkflowRuns';
import { QueryEditorCodeScanning } from './QueryEditorCodeScanning';
import { QueryEditorDeployments } from './QueryEditorDeployments';
import { QueryEditorBranches } from './QueryEditorBranches';

import { DefaultQueryType, QueryTypes } from '../constants';

Expand All @@ -42,6 +43,11 @@ const queryEditors: Record<QueryType, { component: (props: Props, onChange: (val
['Organizations']: { component: () => <></> },
['ProjectItems']: { component: () => <></> },
['Tags']: { component: () => <></> },
['Branches']: {
component: (props: Props, onChange: (val: any) => void) => (
<QueryEditorBranches {...(props.query.options || {})} onChange={onChange} />
),
},
['Releases']: { component: () => <></> },
['Vulnerabilities']: { component: () => <></> },
['Stargazers']: { component: () => <></> },
Expand Down
18 changes: 18 additions & 0 deletions src/views/QueryEditorBranches.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { QueryEditorBranches } from './QueryEditorBranches';
import { render, screen } from '@testing-library/react';

describe('QueryEditorBranches', () => {
it('renders a Filter input field', () => {
const props = { onChange: jest.fn() };
render(<QueryEditorBranches {...props} />);
expect(screen.getByPlaceholderText('release/')).toBeInTheDocument();
});

it('shows existing query value', () => {
const onChange = jest.fn();
render(<QueryEditorBranches query="release/" onChange={onChange} />);
const input = screen.getByPlaceholderText('release/');
expect(input).toHaveValue('release/');
});
});
32 changes: 32 additions & 0 deletions src/views/QueryEditorBranches.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useState } from 'react';
import { Input } from '@grafana/ui';
import { EditorField, EditorRow } from '@grafana/plugin-ui';
import { BranchesOptions } from '../types/query';
import { LeftColumnWidth, RightColumnWidth } from './QueryEditor';

interface Props extends BranchesOptions {
onChange: (value: BranchesOptions) => void;
}

export const QueryEditorBranches = ({ query = '', onChange }: Props) => {
const [filter, setFilter] = useState<string>(query);

return (
<EditorRow>
<EditorField
label="Filter"
tooltip="Filter branches by name prefix (e.g. release/ matches all release/* branches). Leave empty to list all branches."
width={LeftColumnWidth}
>
<Input
aria-label="Branch filter"
placeholder="release/"
value={filter}
onChange={(e) => setFilter(e.currentTarget.value)}
onBlur={() => onChange({ query: filter })}
width={RightColumnWidth}
/>
</EditorField>
</EditorRow>
);
};
Loading