diff --git a/cspell.config.json b/cspell.config.json
index 00b10758..01059904 100644
--- a/cspell.config.json
+++ b/cspell.config.json
@@ -59,6 +59,7 @@
"vals",
"vladimirdotk",
"Wrapf",
- "confg" //this is unfortunately a typo in a file name that is not easy to fix
+ "confg", //this is unfortunately a typo in a file name that is not easy to fix
+ "neovim"
]
}
diff --git a/pkg/github/client/client.go b/pkg/github/client/client.go
index e12742d3..635d0211 100644
--- a/pkg/github/client/client.go
+++ b/pkg/github/client/client.go
@@ -358,3 +358,45 @@ func (client *Client) getWorkflowRuns(ctx context.Context, owner, repo, workflow
return workflowRuns, response.NextPage, nil
}
+
+// GetCopilotMetrics sends a request to the GitHub REST API to get Copilot metrics for an organization or team
+func (client *Client) GetCopilotMetrics(ctx context.Context, organization string, opts models.ListCopilotMetricsOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
+ var u string
+ if opts.TeamSlug != "" {
+ u = fmt.Sprintf("orgs/%s/team/%s/copilot/metrics", organization, opts.TeamSlug)
+ } else {
+ u = fmt.Sprintf("orgs/%s/copilot/metrics", organization)
+ }
+
+ // Build query parameters
+ params := url.Values{}
+ if opts.Since != nil {
+ params.Add("since", opts.Since.Format("2006-01-02"))
+ }
+ if opts.Until != nil {
+ params.Add("until", opts.Until.Format("2006-01-02"))
+ }
+ if opts.Page > 0 {
+ params.Add("page", strconv.Itoa(opts.Page))
+ }
+ if opts.PerPage > 0 {
+ params.Add("per_page", strconv.Itoa(opts.PerPage))
+ }
+
+ if len(params) > 0 {
+ u += "?" + params.Encode()
+ }
+
+ req, err := client.restClient.NewRequest("GET", u, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var metrics []models.CopilotMetrics
+ resp, err := client.restClient.Do(ctx, req, &metrics)
+ if err != nil {
+ return nil, resp, addErrorSourceToError(err, resp)
+ }
+
+ return metrics, resp, nil
+}
diff --git a/pkg/github/codescanning_test.go b/pkg/github/codescanning_test.go
index f3cc8486..b686a5f8 100644
--- a/pkg/github/codescanning_test.go
+++ b/pkg/github/codescanning_test.go
@@ -52,6 +52,10 @@ func (m *mockClient) ListAlertsForOrg(ctx context.Context, owner string, opts *g
return m.mockAlerts, m.mockResponse, nil
}
+func (m *mockClient) GetCopilotMetrics(ctx context.Context, organization string, opts models.ListCopilotMetricsOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
+ return nil, nil, nil
+}
+
func TestGetCodeScanningAlerts(t *testing.T) {
var (
ctx = context.Background()
diff --git a/pkg/github/copilot_metrics.go b/pkg/github/copilot_metrics.go
new file mode 100644
index 00000000..bf33e4d7
--- /dev/null
+++ b/pkg/github/copilot_metrics.go
@@ -0,0 +1,158 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/grafana/github-datasource/pkg/dfutil"
+ "github.com/grafana/github-datasource/pkg/models"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+// CopilotMetricsResponse represents the response from GitHub's Copilot metrics API
+type CopilotMetricsResponse []models.CopilotMetrics
+
+// GetCopilotMetrics retrieves Copilot metrics for an organization or team
+func GetCopilotMetrics(ctx context.Context, client models.Client, opts models.ListCopilotMetricsOptions) (dfutil.Framer, error) {
+ metrics, _, err := client.GetCopilotMetrics(ctx, opts.Organization, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ frameName := "copilot_metrics"
+ if opts.TeamSlug != "" {
+ frameName = "copilot_metrics_team"
+ }
+
+ return copilotMetricsToDataFrame(CopilotMetricsResponse(metrics), frameName)
+}
+
+// copilotMetricsToDataFrame converts Copilot metrics to a Grafana data frame
+func copilotMetricsToDataFrame(metrics CopilotMetricsResponse, name string) (dfutil.Framer, error) {
+ return metrics, nil
+}
+
+// Frames converts the list of copilot metrics to a Grafana DataFrame
+func (c CopilotMetricsResponse) Frames() data.Frames {
+ frame := data.NewFrame("copilot_metrics")
+
+ if len(c) == 0 {
+ return data.Frames{frame}
+ }
+
+ // Create time series for the main metrics
+ dates := make([]time.Time, len(c))
+ totalActiveUsers := make([]int64, len(c))
+ totalEngagedUsers := make([]int64, len(c))
+ ideCompletionUsers := make([]int64, len(c))
+ ideChatUsers := make([]int64, len(c))
+ dotcomChatUsers := make([]int64, len(c))
+ dotcomPRUsers := make([]int64, len(c))
+
+ for i, metric := range c {
+ date, err := time.Parse("2006-01-02", metric.Date)
+ if err != nil {
+ // If date parsing fails, use a default date
+ date = time.Now().AddDate(0, 0, -i)
+ }
+
+ dates[i] = date
+ totalActiveUsers[i] = int64(metric.TotalActiveUsers)
+ totalEngagedUsers[i] = int64(metric.TotalEngagedUsers)
+ ideCompletionUsers[i] = int64(metric.CopilotIDECodeCompletions.TotalEngagedUsers)
+ ideChatUsers[i] = int64(metric.CopilotIDEChat.TotalEngagedUsers)
+ dotcomChatUsers[i] = int64(metric.CopilotDotcomChat.TotalEngagedUsers)
+ dotcomPRUsers[i] = int64(metric.CopilotDotcomPullRequests.TotalEngagedUsers)
+ }
+
+ // Add fields to the frame
+ frame.Fields = append(frame.Fields, data.NewField("time", nil, dates))
+ frame.Fields = append(frame.Fields, data.NewField("total_active_users", nil, totalActiveUsers))
+ frame.Fields = append(frame.Fields, data.NewField("total_engaged_users", nil, totalEngagedUsers))
+ frame.Fields = append(frame.Fields, data.NewField("ide_completion_users", nil, ideCompletionUsers))
+ frame.Fields = append(frame.Fields, data.NewField("ide_chat_users", nil, ideChatUsers))
+ frame.Fields = append(frame.Fields, data.NewField("dotcom_chat_users", nil, dotcomChatUsers))
+ frame.Fields = append(frame.Fields, data.NewField("dotcom_pr_users", nil, dotcomPRUsers))
+
+ // Add language breakdown data if available
+ if len(c) > 0 && len(c[0].CopilotIDECodeCompletions.Languages) > 0 {
+ langData := make(map[string][]int64)
+ for _, metric := range c {
+ for _, lang := range metric.CopilotIDECodeCompletions.Languages {
+ if langData[lang.Name] == nil {
+ langData[lang.Name] = make([]int64, len(c))
+ }
+ }
+ }
+
+ for i, metric := range c {
+ for langName := range langData {
+ found := false
+ for _, lang := range metric.CopilotIDECodeCompletions.Languages {
+ if lang.Name == langName {
+ langData[langName][i] = int64(lang.TotalEngagedUsers)
+ found = true
+ break
+ }
+ }
+ if !found {
+ langData[langName][i] = 0
+ }
+ }
+ }
+
+ for langName, users := range langData {
+ fieldName := fmt.Sprintf("language_%s_users", langName)
+ frame.Fields = append(frame.Fields, data.NewField(fieldName, nil, users))
+ }
+ }
+
+ // Add editor breakdown data if available
+ if len(c) > 0 && len(c[0].CopilotIDECodeCompletions.Editors) > 0 {
+ editorData := make(map[string][]int64)
+ for _, metric := range c {
+ for _, editor := range metric.CopilotIDECodeCompletions.Editors {
+ if editorData[editor.Name] == nil {
+ editorData[editor.Name] = make([]int64, len(c))
+ }
+ }
+ }
+
+ for i, metric := range c {
+ for editorName := range editorData {
+ found := false
+ for _, editor := range metric.CopilotIDECodeCompletions.Editors {
+ if editor.Name == editorName {
+ editorData[editorName][i] = int64(editor.TotalEngagedUsers)
+ found = true
+ break
+ }
+ }
+ if !found {
+ editorData[editorName][i] = 0
+ }
+ }
+ }
+
+ for editorName, users := range editorData {
+ fieldName := fmt.Sprintf("editor_%s_users", editorName)
+ frame.Fields = append(frame.Fields, data.NewField(fieldName, nil, users))
+ }
+ }
+
+ // Add detailed JSON for complex nested data
+ detailedData := make([]string, len(c))
+ for i, metric := range c {
+ jsonData, err := json.Marshal(metric)
+ if err != nil {
+ detailedData[i] = ""
+ } else {
+ detailedData[i] = string(jsonData)
+ }
+ }
+ frame.Fields = append(frame.Fields, data.NewField("detailed_metrics", nil, detailedData))
+
+ return data.Frames{frame}
+}
diff --git a/pkg/github/copilot_metrics_handler.go b/pkg/github/copilot_metrics_handler.go
new file mode 100644
index 00000000..b36e546a
--- /dev/null
+++ b/pkg/github/copilot_metrics_handler.go
@@ -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) handleCopilotMetricsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
+ query := &models.CopilotMetricsQuery{}
+ if err := UnmarshalQuery(q.JSON, query); err != nil {
+ return *err
+ }
+ return dfutil.FrameResponseWithError(s.Datasource.HandleCopilotMetricsQuery(ctx, query, q))
+}
+
+// HandleCopilotMetrics handles the plugin query for GitHub Copilot metrics for an organization or team
+func (s *QueryHandler) HandleCopilotMetrics(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+ return &backend.QueryDataResponse{
+ Responses: processQueries(ctx, req, s.handleCopilotMetricsQuery),
+ }, nil
+}
diff --git a/pkg/github/copilot_metrics_test.go b/pkg/github/copilot_metrics_test.go
new file mode 100644
index 00000000..ed80c161
--- /dev/null
+++ b/pkg/github/copilot_metrics_test.go
@@ -0,0 +1,86 @@
+package github
+
+import (
+ "testing"
+
+ "github.com/grafana/github-datasource/pkg/models"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCopilotMetricsResponse_Frames(t *testing.T) {
+ // Test empty response
+ t.Run("empty response", func(t *testing.T) {
+ response := CopilotMetricsResponse{}
+ frames := response.Frames()
+ assert.Len(t, frames, 1)
+ assert.Equal(t, "copilot_metrics", frames[0].Name)
+ assert.Len(t, frames[0].Fields, 0)
+ })
+
+ // Test response with data
+ t.Run("response with data", func(t *testing.T) {
+ response := CopilotMetricsResponse{
+ {
+ Date: "2025-01-01",
+ TotalActiveUsers: 100,
+ TotalEngagedUsers: 75,
+ CopilotIDECodeCompletions: models.CopilotIDECodeCompletions{
+ TotalEngagedUsers: 50,
+ Languages: []models.CopilotLanguageMetrics{
+ {Name: "go", TotalEngagedUsers: 25},
+ {Name: "typescript", TotalEngagedUsers: 20},
+ },
+ Editors: []models.CopilotEditorMetrics{
+ {Name: "vscode", TotalEngagedUsers: 45},
+ {Name: "neovim", TotalEngagedUsers: 5},
+ },
+ },
+ CopilotIDEChat: models.CopilotIDEChat{
+ TotalEngagedUsers: 30,
+ },
+ CopilotDotcomChat: models.CopilotDotcomChat{
+ TotalEngagedUsers: 25,
+ },
+ CopilotDotcomPullRequests: models.CopilotDotcomPullRequests{
+ TotalEngagedUsers: 15,
+ },
+ },
+ }
+
+ frames := response.Frames()
+ assert.Len(t, frames, 1)
+ frame := frames[0]
+
+ assert.Equal(t, "copilot_metrics", frame.Name)
+
+ // Check that we have the expected fields
+ fieldNames := make([]string, len(frame.Fields))
+ for i, field := range frame.Fields {
+ fieldNames[i] = field.Name
+ }
+
+ expectedFields := []string{
+ "time",
+ "total_active_users",
+ "total_engaged_users",
+ "ide_completion_users",
+ "ide_chat_users",
+ "dotcom_chat_users",
+ "dotcom_pr_users",
+ "language_go_users",
+ "language_typescript_users",
+ "editor_vscode_users",
+ "editor_neovim_users",
+ "detailed_metrics",
+ }
+
+ for _, expected := range expectedFields {
+ assert.Contains(t, fieldNames, expected, "Field %s should be present", expected)
+ }
+
+ // Check that all fields have the correct length
+ for _, field := range frame.Fields {
+ assert.Equal(t, 1, field.Len(), "Field %s should have length 1", field.Name)
+ }
+ })
+}
diff --git a/pkg/github/datasource.go b/pkg/github/datasource.go
index bd8a4196..397c228b 100644
--- a/pkg/github/datasource.go
+++ b/pkg/github/datasource.go
@@ -212,6 +212,12 @@ func (d *Datasource) HandleWorkflowRunsQuery(ctx context.Context, query *models.
return GetWorkflowRuns(ctx, d.client, opt, req.TimeRange)
}
+// HandleCopilotMetricsQuery is the query handler for listing GitHub Copilot metrics for an organization or team
+func (d *Datasource) HandleCopilotMetricsQuery(ctx context.Context, query *models.CopilotMetricsQuery, req backend.DataQuery) (dfutil.Framer, error) {
+ opt := models.CopilotMetricsOptionsWithOrg(query.Options, query.Owner)
+ return GetCopilotMetrics(ctx, d.client, opt)
+}
+
// CheckHealth is the health check for GitHub
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
_, err := GetAllRepositories(ctx, d.client, models.ListRepositoriesOptions{
diff --git a/pkg/github/query_handler.go b/pkg/github/query_handler.go
index 0cf972c5..42717407 100644
--- a/pkg/github/query_handler.go
+++ b/pkg/github/query_handler.go
@@ -62,6 +62,7 @@ func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux {
mux.HandleFunc(models.QueryTypeWorkflowUsage, s.HandleWorkflowUsage)
mux.HandleFunc(models.QueryTypeWorkflowRuns, s.HandleWorkflowRuns)
mux.HandleFunc(models.QueryTypeCodeScanning, s.HandleCodeScanning)
+ mux.HandleFunc(models.QueryTypeCopilotMetrics, s.HandleCopilotMetrics)
return mux
}
diff --git a/pkg/models/client.go b/pkg/models/client.go
index f7897b3e..1b3e3b3c 100644
--- a/pkg/models/client.go
+++ b/pkg/models/client.go
@@ -16,4 +16,5 @@ type Client interface {
GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error)
ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
+ GetCopilotMetrics(ctx context.Context, organization string, opts ListCopilotMetricsOptions) ([]CopilotMetrics, *googlegithub.Response, error)
}
diff --git a/pkg/models/copilot_metrics.go b/pkg/models/copilot_metrics.go
new file mode 100644
index 00000000..5ee94fe0
--- /dev/null
+++ b/pkg/models/copilot_metrics.go
@@ -0,0 +1,130 @@
+package models
+
+import "time"
+
+// ListCopilotMetricsOptions defines the options for listing Copilot metrics for an organization or team
+type ListCopilotMetricsOptions struct {
+ Organization string `json:"organization"`
+ TeamSlug string `json:"team_slug,omitempty"`
+ Since *time.Time `json:"since,omitempty"`
+ Until *time.Time `json:"until,omitempty"`
+ Page int `json:"page,omitempty"`
+ PerPage int `json:"per_page,omitempty"`
+}
+
+// CopilotMetrics represents a daily metrics record for Copilot usage
+type CopilotMetrics struct {
+ Date string `json:"date"`
+ TotalActiveUsers int `json:"total_active_users"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ CopilotIDECodeCompletions CopilotIDECodeCompletions `json:"copilot_ide_code_completions"`
+ CopilotIDEChat CopilotIDEChat `json:"copilot_ide_chat"`
+ CopilotDotcomChat CopilotDotcomChat `json:"copilot_dotcom_chat"`
+ CopilotDotcomPullRequests CopilotDotcomPullRequests `json:"copilot_dotcom_pull_requests"`
+}
+
+// CopilotIDECodeCompletions represents code completion metrics in IDEs
+type CopilotIDECodeCompletions struct {
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Languages []CopilotLanguageMetrics `json:"languages"`
+ Editors []CopilotEditorMetrics `json:"editors"`
+}
+
+// CopilotIDEChat represents chat metrics in IDEs
+type CopilotIDEChat struct {
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Editors []CopilotEditorChatMetrics `json:"editors"`
+}
+
+// CopilotDotcomChat represents chat metrics on GitHub.com
+type CopilotDotcomChat struct {
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Models []CopilotChatModel `json:"models"`
+}
+
+// CopilotDotcomPullRequests represents pull request metrics on GitHub.com
+type CopilotDotcomPullRequests struct {
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Repositories []CopilotRepositoryMetrics `json:"repositories"`
+}
+
+// CopilotLanguageMetrics represents usage metrics for a specific language
+type CopilotLanguageMetrics struct {
+ Name string `json:"name"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ TotalCodeSuggestions int `json:"total_code_suggestions,omitempty"`
+ TotalCodeAcceptances int `json:"total_code_acceptances,omitempty"`
+ TotalCodeLinesSuggested int `json:"total_code_lines_suggested,omitempty"`
+ TotalCodeLinesAccepted int `json:"total_code_lines_accepted,omitempty"`
+}
+
+// CopilotEditorMetrics represents usage metrics for a specific editor
+type CopilotEditorMetrics struct {
+ Name string `json:"name"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Models []CopilotEditorModel `json:"models"`
+}
+
+// CopilotEditorChatMetrics represents chat metrics for a specific editor
+type CopilotEditorChatMetrics struct {
+ Name string `json:"name"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Models []CopilotEditorChatModel `json:"models"`
+}
+
+// CopilotEditorModel represents model metrics for a specific editor
+type CopilotEditorModel struct {
+ Name string `json:"name"`
+ IsCustomModel bool `json:"is_custom_model"`
+ CustomModelTrainingDate *string `json:"custom_model_training_date"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Languages []CopilotLanguageMetrics `json:"languages"`
+}
+
+// CopilotEditorChatModel represents chat model metrics for a specific editor
+type CopilotEditorChatModel struct {
+ Name string `json:"name"`
+ IsCustomModel bool `json:"is_custom_model"`
+ CustomModelTrainingDate *string `json:"custom_model_training_date"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ TotalChats int `json:"total_chats"`
+ TotalChatInsertionEvents int `json:"total_chat_insertion_events"`
+ TotalChatCopyEvents int `json:"total_chat_copy_events"`
+}
+
+// CopilotChatModel represents chat model metrics for GitHub.com
+type CopilotChatModel struct {
+ Name string `json:"name"`
+ IsCustomModel bool `json:"is_custom_model"`
+ CustomModelTrainingDate *string `json:"custom_model_training_date"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ TotalChats int `json:"total_chats"`
+}
+
+// CopilotRepositoryMetrics represents metrics for a specific repository
+type CopilotRepositoryMetrics struct {
+ Name string `json:"name"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+ Models []CopilotRepositoryModel `json:"models"`
+}
+
+// CopilotRepositoryModel represents model metrics for a specific repository
+type CopilotRepositoryModel struct {
+ Name string `json:"name"`
+ IsCustomModel bool `json:"is_custom_model"`
+ CustomModelTrainingDate *string `json:"custom_model_training_date"`
+ TotalPRSummariesCreated int `json:"total_pr_summaries_created"`
+ TotalEngagedUsers int `json:"total_engaged_users"`
+}
+
+// CopilotMetricsOptionsWithOrg adds the Owner value to a ListCopilotMetricsOptions. This is a convenience function because this is a common operation
+func CopilotMetricsOptionsWithOrg(opt ListCopilotMetricsOptions, owner string) ListCopilotMetricsOptions {
+ return ListCopilotMetricsOptions{
+ Organization: owner,
+ TeamSlug: opt.TeamSlug,
+ Since: opt.Since,
+ Until: opt.Until,
+ Page: opt.Page,
+ PerPage: opt.PerPage,
+ }
+}
diff --git a/pkg/models/query.go b/pkg/models/query.go
index 88292805..dbe5158e 100644
--- a/pkg/models/query.go
+++ b/pkg/models/query.go
@@ -43,6 +43,8 @@ const (
QueryTypeWorkflowRuns = "Workflow_Runs"
// QueryTypeCodeScanning is used when querying code scanning alerts for a repository
QueryTypeCodeScanning = "Code_Scanning"
+ // QueryTypeCopilotMetrics is used when querying Copilot metrics for an organization or team
+ QueryTypeCopilotMetrics = "Copilot_Metrics"
)
// Query refers to the structure of a query built using the QueryEditor.
@@ -125,6 +127,12 @@ type VulnerabilityQuery struct {
Options ListVulnerabilitiesOptions `json:"options"`
}
+// CopilotMetricsQuery is used when querying Copilot metrics for an organization or team
+type CopilotMetricsQuery struct {
+ Query
+ Options ListCopilotMetricsOptions `json:"options"`
+}
+
// StargazersQuery is used when querying stargazers for a repository
type StargazersQuery struct {
Query
diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go
index ee7ce469..e154e47f 100644
--- a/pkg/plugin/datasource.go
+++ b/pkg/plugin/datasource.go
@@ -28,6 +28,7 @@ type Datasource interface {
HandleWorkflowsQuery(context.Context, *models.WorkflowsQuery, backend.DataQuery) (dfutil.Framer, error)
HandleWorkflowUsageQuery(context.Context, *models.WorkflowUsageQuery, backend.DataQuery) (dfutil.Framer, error)
HandleWorkflowRunsQuery(context.Context, *models.WorkflowRunsQuery, backend.DataQuery) (dfutil.Framer, error)
+ HandleCopilotMetricsQuery(context.Context, *models.CopilotMetricsQuery, backend.DataQuery) (dfutil.Framer, error)
CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error)
QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
}
diff --git a/pkg/plugin/datasource_caching.go b/pkg/plugin/datasource_caching.go
index 1c19970f..ccacea71 100644
--- a/pkg/plugin/datasource_caching.go
+++ b/pkg/plugin/datasource_caching.go
@@ -262,6 +262,16 @@ func (c *CachedDatasource) HandleWorkflowRunsQuery(ctx context.Context, q *model
return c.saveCache(req, f, err)
}
+// HandleCopilotMetricsQuery is the cache wrapper for the Copilot metrics query handler
+func (c *CachedDatasource) HandleCopilotMetricsQuery(ctx context.Context, q *models.CopilotMetricsQuery, req backend.DataQuery) (dfutil.Framer, error) {
+ if value, err := c.getCache(req); err == nil {
+ return value, err
+ }
+
+ f, err := c.datasource.HandleCopilotMetricsQuery(ctx, q, req)
+ return c.saveCache(req, f, err)
+}
+
// CheckHealth forwards the request to the datasource and does not perform any caching
func (c *CachedDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return c.datasource.CheckHealth(ctx, req)
diff --git a/pkg/testutil/client.go b/pkg/testutil/client.go
index 13aef4fb..0bc4b7a3 100644
--- a/pkg/testutil/client.go
+++ b/pkg/testutil/client.go
@@ -76,3 +76,8 @@ func (c *TestClient) ListAlertsForRepo(ctx context.Context, owner, repo string,
func (c *TestClient) ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) {
panic("unimplemented")
}
+
+// GetCopilotMetrics is not implemented because it is not being used in tests at the moment.
+func (c *TestClient) GetCopilotMetrics(ctx context.Context, organization string, opts models.ListCopilotMetricsOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
+ panic("unimplemented")
+}
diff --git a/src/constants.ts b/src/constants.ts
index a141e8cc..a190feab 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -20,6 +20,7 @@ export enum QueryType {
Workflows = 'Workflows',
Workflow_Usage = 'Workflow_Usage',
Workflow_Runs = 'Workflow_Runs',
+ Copilot_Metrics = 'Copilot_Metrics',
}
export const DefaultQueryType = QueryType.Issues;
diff --git a/src/types/query.ts b/src/types/query.ts
index 1694020f..26a5de19 100644
--- a/src/types/query.ts
+++ b/src/types/query.ts
@@ -20,7 +20,8 @@ export interface GitHubQuery extends Indexable, DataQuery, RepositoryOptions {
| ProjectsOptions
| WorkflowsOptions
| WorkflowUsageOptions
- | WorkflowRunsOptions;
+ | WorkflowRunsOptions
+ | CopilotMetricsOptions;
}
export interface Label {
@@ -108,3 +109,12 @@ export interface GitHubVariableQuery extends GitHubQuery {
export interface GitHubAnnotationQuery extends GitHubVariableQuery {
timeField?: string;
}
+
+export interface CopilotMetricsOptions extends Indexable {
+ organization?: string;
+ teamSlug?: string;
+ since?: string;
+ until?: string;
+ page?: number;
+ perPage?: number;
+}
diff --git a/src/views/QueryEditor.tsx b/src/views/QueryEditor.tsx
index f94f60cd..20ca0180 100644
--- a/src/views/QueryEditor.tsx
+++ b/src/views/QueryEditor.tsx
@@ -23,6 +23,7 @@ import QueryEditorWorkflows from './QueryEditorWorkflows';
import QueryEditorWorkflowUsage from './QueryEditorWorkflowUsage';
import QueryEditorWorkflowRuns from './QueryEditorWorkflowRuns';
import QueryEditorCodeScanning from './QueryEditorCodeScanning';
+import QueryEditorCopilotMetrics from './QueryEditorCopilotMetrics';
import { QueryType, DefaultQueryType } from '../constants';
import type { GitHubQuery } from '../types/query';
import type { GitHubDataSourceOptions } from '../types/config';
@@ -117,6 +118,11 @@ const queryEditors: {
),
},
+ [QueryType.Copilot_Metrics]: {
+ component: (props: Props, onChange: (val: any) => void) => (
+
+ ),
+ },
};
/* eslint-enable react/display-name */
@@ -189,7 +195,7 @@ const QueryEditor = (props: Props) => {
);
};
-const nonRepoTypes = [QueryType.Projects, QueryType.ProjectItems];
+const nonRepoTypes = [QueryType.Projects, QueryType.ProjectItems, QueryType.Copilot_Metrics];
function hasRepo(qt?: string) {
return !nonRepoTypes.includes(qt as QueryType);
diff --git a/src/views/QueryEditorCopilotMetrics.tsx b/src/views/QueryEditorCopilotMetrics.tsx
new file mode 100644
index 00000000..b17b2042
--- /dev/null
+++ b/src/views/QueryEditorCopilotMetrics.tsx
@@ -0,0 +1,48 @@
+import React, { useState } from 'react';
+import { Input, InlineField } from '@grafana/ui';
+import { RightColumnWidth, LeftColumnWidth } from './QueryEditor';
+import type { CopilotMetricsOptions } from '../types/query';
+import { components } from '../components/selectors';
+
+interface Props extends CopilotMetricsOptions {
+ onChange: (val: CopilotMetricsOptions) => void;
+}
+
+const QueryEditorCopilotMetrics = (props: Props) => {
+ const [team, setTeam] = useState(props.teamSlug || '');
+ const [owner, setOwner] = useState(props.owner || '');
+
+ return (
+ <>
+ setOwner(el.currentTarget.value)}
+ onBlur={(el) =>
+ props.onChange({
+ ...props,
+ owner: el.currentTarget.value,
+ })
+ }
+ />
+
+ setTeam(el.currentTarget.value)}
+ onBlur={(el) =>
+ props.onChange({
+ ...props,
+ teamSlug: el.currentTarget.value
+ })
+ }
+ placeholder="Enter team slug (optional)"
+ />
+
+ >
+ );
+};
+
+export default QueryEditorCopilotMetrics;