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;