diff --git a/CHANGELOG.md b/CHANGELOG.md index dac7032f..ae36e64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 2.7.1 + +⚙️ Updating experimental schemas. + ## 2.7.0 🚀 Adding support for experimental schemas. diff --git a/cspell.config.json b/cspell.config.json index c45504e7..b6f51196 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -62,6 +62,8 @@ "confg", "octocat", "schemads", - "featuretoggles" + "featuretoggles", + "oldorg", + "oldrepo" ] } diff --git a/go.mod b/go.mod index 59a0a928..ada1a0bc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 github.com/google/go-github/v81 v81.0.0 github.com/grafana/grafana-plugin-sdk-go v0.290.0 - github.com/grafana/schemads v0.0.1 + github.com/grafana/schemads v0.0.5 github.com/influxdata/tdigest v0.0.1 github.com/pkg/errors v0.9.1 github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed diff --git a/go.sum b/go.sum index 9c777db1..a2b3979e 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/grafana/schemads v0.0.1 h1:hw+8zJlZG/dFPbAlXs+PU86pwqG2tPO1mL8ws0S8/Do= -github.com/grafana/schemads v0.0.1/go.mod h1:i/iRKic1c9i/ZjApKe7+BDQjPhlQO7gWycVRu7x6c1U= +github.com/grafana/schemads v0.0.5 h1:nVDpWTb+NPHmUCOaqm38pwg+JDJxb28rd6j4+7kwYD4= +github.com/grafana/schemads v0.0.5/go.mod h1:i/iRKic1c9i/ZjApKe7+BDQjPhlQO7gWycVRu7x6c1U= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= diff --git a/package.json b/package.json index c7a8ca3c..75edfb9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grafana-github-datasource", - "version": "2.7.0", + "version": "2.7.1", "private": true, "description": "Grafana data source plugin for Github", "repository": "github:grafana/github-datasource", diff --git a/pkg/github/schema.go b/pkg/github/schema.go index 9f672588..a4d540dd 100644 --- a/pkg/github/schema.go +++ b/pkg/github/schema.go @@ -33,10 +33,6 @@ var ( {Name: "organization", Root: true, Required: true}, } - projectTableParameters = []schemas.TableParameter{ - {Name: "organization", Root: true, Required: false}, - } - workflowUsageTableParameters = []schemas.TableParameter{ {Name: "organization", Root: true, Required: true}, {Name: "repository", DependsOn: []string{"organization"}, Required: true}, @@ -395,7 +391,7 @@ func getAllTables() []schemas.Table { }, { Name: normalizeTableNames(models.QueryTypeProjects), - TableParameters: projectTableParameters, + TableParameters: orgOnlyTableParameters, Columns: []schemas.Column{ {Name: "number", Type: schemas.ColumnTypeInt64}, {Name: "title", Type: schemas.ColumnTypeString}, @@ -408,18 +404,6 @@ func getAllTables() []schemas.Table { {Name: "short_description", Type: schemas.ColumnTypeString}, }, }, - { - Name: normalizeTableNames(models.QueryTypeProjectItems), - TableParameters: projectTableParameters, - Columns: []schemas.Column{ - {Name: "id", Type: schemas.ColumnTypeString}, - {Name: "archived", Type: schemas.ColumnTypeBoolean}, - {Name: "type", Type: schemas.ColumnTypeString}, - {Name: "updated_at", Type: schemas.ColumnTypeDatetime}, - {Name: "created_at", Type: schemas.ColumnTypeDatetime}, - {Name: "closed_at", Type: schemas.ColumnTypeDatetime}, - }, - }, { Name: normalizeTableNames(models.QueryTypeStargazers), TableParameters: repoScopedTableParameters, diff --git a/pkg/github/sql.go b/pkg/github/sql.go index 3ad86c59..7dfb5af8 100644 --- a/pkg/github/sql.go +++ b/pkg/github/sql.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/github-datasource/pkg/models" "github.com/grafana/grafana-plugin-sdk-go/backend" + schemas "github.com/grafana/schemads" ) // tableToQueryType maps normalized table names to their QueryType constants. @@ -26,7 +27,6 @@ var tableToQueryType = func() map[string]string { models.QueryTypePackages, models.QueryTypeVulnerabilities, models.QueryTypeProjects, - models.QueryTypeProjectItems, models.QueryTypeStargazers, models.QueryTypeWorkflows, models.QueryTypeWorkflowUsage, @@ -54,207 +54,151 @@ func parseJSONStringValues(s string) []string { return []string{s} } +func extractFilterValues(condition schemas.FilterCondition) []string { + out := make([]string, 0, len(condition.Values)+1) + for _, v := range condition.Values { + if v != "" { + out = append(out, v) + } + } + if len(out) == 0 && condition.Value != "" { + out = append(out, condition.Value) + } + return out +} + // applyFilters maps SQL filter predicates to GitHub API query options. // It modifies the options map in-place and returns a list of GitHub search // qualifiers for query types that use the search API. -func applyFilters(queryType string, options map[string]interface{}, filters []map[string]interface{}) []string { +func applyFilters(queryType string, options map[string]interface{}, filters []schemas.ColumnFilter) []string { var searchQualifiers []string - for _, f := range filters { - column, _ := f["column"].(string) - op, _ := f["op"].(string) - value, _ := f["value"].(string) - if column == "" || value == "" { - continue - } + opts, _ := options["options"].(map[string]interface{}) + if opts == nil { + opts = make(map[string]interface{}) + options["options"] = opts + } - switch queryType { - case models.QueryTypeIssues: - switch column { - case "state": - if op == "==" || op == "=" { - searchQualifiers = append(searchQualifiers, "state:"+value) - } - case "author": - if op == "==" || op == "=" { - searchQualifiers = append(searchQualifiers, "author:"+value) - } - case "labels": - if op == "==" || op == "=" { + appendEqualitySearchQualifier := func(name string, operator schemas.Operator, values []string, isJSON bool) { + if operator == schemas.OperatorEquals || operator == schemas.OperatorIn { + for _, value := range values { + if isJSON { for _, v := range parseJSONStringValues(value) { - searchQualifiers = append(searchQualifiers, "label:"+v) + searchQualifiers = append(searchQualifiers, name+":"+v) } - } - case "assignees": - if op == "==" || op == "=" { - for _, v := range parseJSONStringValues(value) { - searchQualifiers = append(searchQualifiers, "assignee:"+v) - } - } - case "milestone": - if op == "==" || op == "=" { - searchQualifiers = append(searchQualifiers, "milestone:"+value) + } else { + searchQualifiers = append(searchQualifiers, name+":"+value) } } + } + } + setOption := func(name string, operator schemas.Operator, value string) { + if operator == schemas.OperatorEquals || operator == schemas.OperatorIn { + opts[name] = value + } + } - case models.QueryTypePullRequests, models.QueryTypePullRequestReviews: - switch column { - case "state": - if op == "==" || op == "=" { - searchQualifiers = append(searchQualifiers, "state:"+value) - } - case "author_login": - if op == "==" || op == "=" { - searchQualifiers = append(searchQualifiers, "author:"+value) - } - case "labels": - if op == "==" || op == "=" { - for _, v := range parseJSONStringValues(value) { - searchQualifiers = append(searchQualifiers, "label:"+v) - } - } - case "is_draft": - if op == "==" || op == "=" { - if value == "true" { - searchQualifiers = append(searchQualifiers, "draft:true") - } else { - searchQualifiers = append(searchQualifiers, "draft:false") - } - } - } + for _, f := range filters { + if f.Name == "" || len(f.Conditions) == 0 { + continue + } - case models.QueryTypeCodeScanning: - switch column { - case "state": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } - opts["state"] = value - } - case "rule_severity": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } - opts["severity"] = value - } - case "tool_name": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } - opts["toolName"] = value - } + for _, condition := range f.Conditions { + values := extractFilterValues(condition) + if len(values) == 0 { + continue } - case models.QueryTypeWorkflowRuns: - switch column { - case "head_branch": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } - opts["branch"] = value + switch queryType { + case models.QueryTypeIssues: + switch f.Name { + case "state": + appendEqualitySearchQualifier(f.Name, condition.Operator, values, false) + case "author": + appendEqualitySearchQualifier(f.Name, condition.Operator, values, false) + case "labels": + appendEqualitySearchQualifier("label", condition.Operator, values, true) + case "assignees": + appendEqualitySearchQualifier("assignee", condition.Operator, values, true) + case "milestone": + appendEqualitySearchQualifier("milestone", condition.Operator, values, false) } - case "status": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts + case models.QueryTypePullRequests, models.QueryTypePullRequestReviews: + switch f.Name { + case "state": + appendEqualitySearchQualifier("state", condition.Operator, values, false) + case "author_login": + appendEqualitySearchQualifier("author", condition.Operator, values, false) + case "labels": + appendEqualitySearchQualifier("label", condition.Operator, values, true) + case "is_draft": + if condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn { + for _, value := range values { + if value == "true" { + searchQualifiers = append(searchQualifiers, "draft:true") + } else { + searchQualifiers = append(searchQualifiers, "draft:false") + } + } } - opts["status"] = value } - case "event": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } - opts["event"] = value + case models.QueryTypeCodeScanning: + switch f.Name { + case "state": + setOption("state", condition.Operator, values[0]) + case "rule_severity": + setOption("severity", condition.Operator, values[0]) + case "tool_name": + setOption("toolName", condition.Operator, values[0]) } - } - - case models.QueryTypeContributors: - if column == "name" && (op == "like" || op == "==" || op == "=") { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts + case models.QueryTypeWorkflowRuns: + switch f.Name { + case "head_branch": + setOption("branch", condition.Operator, values[0]) + case "status": + setOption("status", condition.Operator, values[0]) + case "event": + setOption("event", condition.Operator, values[0]) } - opts["query"] = value - } - - case models.QueryTypeLabels: - if column == "name" && (op == "like" || op == "==" || op == "=") { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts + case models.QueryTypeContributors: + if f.Name == "name" && (condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn) { + opts["query"] = values[0] } - opts["query"] = value - } - - case models.QueryTypeMilestones: - if column == "title" && (op == "like" || op == "==" || op == "=") { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts + case models.QueryTypeLabels: + if f.Name == "name" && (condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn) { + opts["query"] = values[0] } - opts["query"] = value - } - - case models.QueryTypePackages: - switch column { - case "name": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts + case models.QueryTypeMilestones: + if f.Name == "title" && (condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn) { + opts["query"] = values[0] + } + case models.QueryTypePackages: + switch f.Name { + case "name": + if condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn { + for _, value := range values { + existing, _ := opts["names"].(string) + if existing != "" { + opts["names"] = existing + "," + value + } else { + opts["names"] = value + } + } } - existing, _ := opts["names"].(string) - if existing != "" { - opts["names"] = existing + "," + value - } else { - opts["names"] = value + case "type": + if condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn { + opts["packageType"] = values[0] } } - case "type": - if op == "==" || op == "=" { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } - opts["packageType"] = value + case models.QueryTypeRepositories: + if f.Name == "name" && (condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn) { + options["repository"] = values[0] } } - - case models.QueryTypeRepositories: - if column == "name" && (op == "like" || op == "==" || op == "=") { - options["repository"] = value - } } } if len(searchQualifiers) > 0 { - opts, _ := options["options"].(map[string]interface{}) - if opts == nil { - opts = make(map[string]interface{}) - options["options"] = opts - } existing, _ := opts["query"].(string) combined := strings.Join(searchQualifiers, " ") if existing != "" { @@ -275,19 +219,17 @@ func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDat grafanaConfig := req.PluginContext.GrafanaConfig queries := make([]backend.DataQuery, 0, len(req.Queries)) for _, q := range req.Queries { - var raw map[string]interface{} - if err := json.Unmarshal(q.JSON, &raw); err != nil { + var query schemas.GenericQuery + if err := json.Unmarshal(q.JSON, &query); err != nil { queries = append(queries, q) continue } - grafanaSql, _ := raw["grafanaSql"].(bool) - table, _ := raw["table"].(string) - if !grafanaSql || table == "" { + if !query.GrafanaSql || query.Table == "" { queries = append(queries, q) continue } - if grafanaSql { + if query.GrafanaSql { if grafanaConfig == nil { backend.Logger.Warn("grafanaConfig is not set, skipping query") continue @@ -301,7 +243,7 @@ func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDat // Table names use hyphens only (never underscores), so the first // underscore unambiguously separates the table name from the // owner/repo suffix: "issues_grafana_grafana" -> "issues" + "grafana_grafana". - parts := strings.SplitN(table, "_", 2) + parts := strings.SplitN(query.Table, "_", 2) queryType, ok := tableToQueryType[parts[0]] if !ok { queries = append(queries, q) @@ -316,24 +258,32 @@ func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDat repo = ownerRepo[1] } } + if v := strings.TrimSpace(query.TableParameterValues["organization"]); v != "" { + owner = v + } + if v := strings.TrimSpace(query.TableParameterValues["repository"]); v != "" { + repo = v + } normalized := map[string]interface{}{ - "refId": raw["refId"], - "datasource": raw["datasource"], + "refId": query.RefID, + "datasource": query.Datasource, "queryType": queryType, "owner": owner, "repository": repo, "options": map[string]interface{}{}, } + if queryType == models.QueryTypeProjects && owner != "" { + opts, _ := normalized["options"].(map[string]interface{}) + opts["organization"] = owner + } + if v := strings.TrimSpace(query.TableParameterValues["workflow"]); v != "" { + opts, _ := normalized["options"].(map[string]interface{}) + opts["workflow"] = v + } - if filters, ok := raw["filters"].([]interface{}); ok && len(filters) > 0 { - filterMaps := make([]map[string]interface{}, 0, len(filters)) - for _, f := range filters { - if fm, ok := f.(map[string]interface{}); ok { - filterMaps = append(filterMaps, fm) - } - } - applyFilters(queryType, normalized, filterMaps) + if len(query.Filters) > 0 { + applyFilters(queryType, normalized, query.Filters) } jsonBytes, err := json.Marshal(normalized) diff --git a/pkg/github/sql_handler_test.go b/pkg/github/sql_handler_test.go index 33a07e25..e8163f97 100644 --- a/pkg/github/sql_handler_test.go +++ b/pkg/github/sql_handler_test.go @@ -25,7 +25,7 @@ func TestTableToQueryTypeCoversAllTypes(t *testing.T) { models.QueryTypeTags, models.QueryTypeReleases, models.QueryTypeLabels, models.QueryTypeMilestones, models.QueryTypePackages, models.QueryTypeVulnerabilities, - models.QueryTypeProjects, models.QueryTypeProjectItems, + models.QueryTypeProjects, models.QueryTypeStargazers, models.QueryTypeWorkflows, models.QueryTypeWorkflowUsage, models.QueryTypeWorkflowRuns, models.QueryTypeCodeScanning, models.QueryTypeOrganizations, @@ -239,6 +239,120 @@ func TestNormalizeGrafanaSQLRequest(t *testing.T) { } }) + t.Run("maps projects owner to options.organization", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"projects_grafana"}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + if out == nil || len(out.Queries) != 1 { + t.Fatalf("expected one query") + } + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts, _ := raw["options"].(map[string]interface{}) + org, _ := opts["organization"].(string) + if org != "grafana" { + t.Errorf("expected options.organization 'grafana', got %q", org) + } + }) + + t.Run("uses tableParameterValues for owner and repository", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues","tableParameterValues":{"organization":"grafana","repository":"grafana"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + if out == nil || len(out.Queries) != 1 { + t.Fatalf("expected one query") + } + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + if raw["owner"] != "grafana" || raw["repository"] != "grafana" { + t.Errorf("owner/repository: got %v / %v", raw["owner"], raw["repository"]) + } + }) + + t.Run("tableParameterValues override owner and repository from table suffix", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_oldorg_oldrepo","tableParameterValues":{"organization":"grafana","repository":"grafana"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + if out == nil || len(out.Queries) != 1 { + t.Fatalf("expected one query") + } + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + if raw["owner"] != "grafana" || raw["repository"] != "grafana" { + t.Errorf("owner/repository: got %v / %v", raw["owner"], raw["repository"]) + } + }) + + t.Run("maps workflow table parameter to options.workflow", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflow-usage","tableParameterValues":{"organization":"grafana","repository":"grafana","workflow":"build.yml"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + if out == nil || len(out.Queries) != 1 { + t.Fatalf("expected one query") + } + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + if raw["owner"] != "grafana" || raw["repository"] != "grafana" { + t.Errorf("owner/repository: got %v / %v", raw["owner"], raw["repository"]) + } + opts, _ := raw["options"].(map[string]interface{}) + workflow, _ := opts["workflow"].(string) + if workflow != "build.yml" { + t.Errorf("expected options.workflow 'build.yml', got %q", workflow) + } + }) + + t.Run("maps projects organization from tableParameterValues", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"projects","tableParameterValues":{"organization":"grafana"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + if out == nil || len(out.Queries) != 1 { + t.Fatalf("expected one query") + } + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts, _ := raw["options"].(map[string]interface{}) + org, _ := opts["organization"].(string) + if org != "grafana" { + t.Errorf("expected options.organization 'grafana', got %q", org) + } + }) + t.Run("leaves non-grafanaSql query unchanged", func(t *testing.T) { queryJSON := []byte(`{"refId":"A","queryType":"Pull_Requests","owner":"grafana","repository":"grafana"}`) req := &backend.QueryDataRequest{ @@ -315,7 +429,7 @@ func TestNormalizeGrafanaSQLRequest(t *testing.T) { func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { t.Run("pushes down state filter for issues", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"column":"state","op":"==","value":"open"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"name":"state","conditions":[{"operator":"=","value":"open"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -341,7 +455,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down author filter for issues", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"column":"author","op":"==","value":"octocat"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"name":"author","conditions":[{"operator":"=","value":"octocat"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -361,7 +475,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down multiple filters for issues", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"column":"state","op":"==","value":"open"},{"column":"labels","op":"==","value":"bug"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"name":"state","conditions":[{"operator":"=","value":"open"}]},{"name":"labels","conditions":[{"operator":"=","value":"bug"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -380,8 +494,48 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { } }) + t.Run("pushes down JSON array value for issues labels", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"name":"labels","conditions":[{"operator":"in","values":["bug","triage"]}]}]}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + query := opts["query"].(string) + if query != "label:bug label:triage" { + t.Errorf("expected query 'label:bug label:triage', got %q", query) + } + }) + + t.Run("pushes down IN values for assignees", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"name":"assignees","conditions":[{"operator":"in","values":["alice","bob"]}]}]}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + query := opts["query"].(string) + if query != "assignee:alice assignee:bob" { + t.Errorf("expected query 'assignee:alice assignee:bob', got %q", query) + } + }) + t.Run("pushes down state filter for code-scanning", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"code-scanning_grafana_grafana","filters":[{"column":"state","op":"==","value":"open"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"code-scanning_grafana_grafana","filters":[{"name":"state","conditions":[{"operator":"=","value":"open"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -401,7 +555,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down branch filter for workflow-runs", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflow-runs_grafana_grafana","filters":[{"column":"head_branch","op":"==","value":"main"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflow-runs_grafana_grafana","filters":[{"name":"head_branch","conditions":[{"operator":"=","value":"main"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -421,7 +575,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down status filter for workflow-runs", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflow-runs_grafana_grafana","filters":[{"column":"status","op":"==","value":"completed"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflow-runs_grafana_grafana","filters":[{"name":"status","conditions":[{"operator":"=","value":"completed"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -441,7 +595,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down draft filter for pull-requests", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"pull-requests_grafana_grafana","filters":[{"column":"is_draft","op":"==","value":"true"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"pull-requests_grafana_grafana","filters":[{"name":"is_draft","conditions":[{"operator":"=","value":"true"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -461,7 +615,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down name filter for labels (like)", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"labels_grafana_grafana","filters":[{"column":"name","op":"like","value":"bug"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"labels_grafana_grafana","filters":[{"name":"name","conditions":[{"operator":"like","value":"bug"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -481,7 +635,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down package type filter", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"packages_grafana_grafana","filters":[{"column":"type","op":"==","value":"DOCKER"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"packages_grafana_grafana","filters":[{"name":"type","conditions":[{"operator":"=","value":"DOCKER"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -501,7 +655,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down name filter for repositories", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"repositories_grafana","filters":[{"column":"name","op":"like","value":"grafana"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"repositories_grafana","filters":[{"name":"name","conditions":[{"operator":"like","value":"grafana"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -519,7 +673,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("pushes down tool_name filter for code-scanning", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"code-scanning_grafana_grafana","filters":[{"column":"tool_name","op":"==","value":"CodeQL"}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"code-scanning_grafana_grafana","filters":[{"name":"tool_name","conditions":[{"operator":"=","value":"CodeQL"}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{ @@ -539,7 +693,7 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { }) t.Run("ignores filters with empty values", func(t *testing.T) { - queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"column":"state","op":"==","value":""}]}`) + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana","filters":[{"name":"state","conditions":[{"operator":"=","value":""}]}]}`) req := &backend.QueryDataRequest{ PluginContext: pluginCtxWithFeatureToggle(), Queries: []backend.DataQuery{