Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/** linguist-vendored
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
# Changelog
## 1.3.1 (2026-03-15)
* Improve project dropdown performance with AsyncSelect and server-side search
* Cache default project list to reduce redundant backend requests
* Cap ListProjects results to 100 for performance
* Use Grafana UI components (Field, Input, Alert) in ConfigEditor and QueryEditor
* Fix log levels from Warn to Error for GCE and ListProjects errors
* Add request URL parsing for project search query support
* Add .gitattributes for GitHub language statistics
* Enhance filterQuery to skip empty traceID and missing projectId queries
* Add null safety guard in addLinksToTraceIdColumn
* Persist default query values via onChange in QueryEditor
* Fix resource leak: close trace client when resource manager init fails
* Fix direct props mutation in ConfigEditor service account impersonation

## 1.3.0 (2026-03-09)
* Support OAuth passthrough authentication
* Add universe domain support
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "googlecloud-trace-datasource",
"version": "1.3.0",
"version": "1.3.1",
"description": "Backend Grafana plugin that enables visualization of GCP Cloud Trace traces and spans in Grafana.",
"scripts": {
"postinstall": "rm -rf node_modules/flatted/golang",
Expand Down
27 changes: 22 additions & 5 deletions pkg/plugin/cloudtrace/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ type API interface {
GetTrace(context.Context, *TraceQuery) (*cloudtracepb.Trace, error)
// TestConnection queries for any trace from the given project
TestConnection(ctx context.Context, projectID string) error
// ListProjects returns the project IDs of all visible projects
ListProjects(context.Context) ([]string, error)
// ListProjects returns the project IDs of all visible projects.
// If query is non-empty it is forwarded to the Resource Manager search filter.
ListProjects(ctx context.Context, query string) ([]string, error)
// Close closes the underlying connection to the GCP API
Close() error
}
Expand Down Expand Up @@ -79,6 +80,7 @@ func NewClient(ctx context.Context, jsonCreds []byte, universeDomain string) (*C
}
rClient, err := resourcemanager.NewProjectsClient(ctx, opts...)
if err != nil {
_ = client.Close()
return nil, err
}

Expand All @@ -100,6 +102,7 @@ func NewClientWithGCE(ctx context.Context, universeDomain string) (*Client, erro
}
rClient, err := resourcemanager.NewProjectsClient(ctx, opts...)
if err != nil {
_ = client.Close()
return nil, err
}

Expand Down Expand Up @@ -143,6 +146,7 @@ func NewClientWithImpersonation(ctx context.Context, jsonCreds []byte, impersona
}
rClient, err := resourcemanager.NewProjectsClient(ctx, opts...)
if err != nil {
_ = client.Close()
return nil, err
}

Expand All @@ -169,6 +173,7 @@ func NewClientWithAccessToken(ctx context.Context, accessToken string, universeD

rClient, err := resourcemanager.NewProjectsClient(ctx, opts...)
if err != nil {
_ = client.Close()
return nil, err
}

Expand Down Expand Up @@ -200,6 +205,7 @@ func NewClientWithPassThrough(ctx context.Context, headers map[string]string, un
}
rClient, err := resourcemanager.NewProjectsClient(ctx, opts...)
if err != nil {
_ = client.Close()
return nil, err
}

Expand Down Expand Up @@ -229,10 +235,18 @@ type TraceQuery struct {
TraceID string
}

// ListProjects returns the project IDs of all visible projects
func (c *Client) ListProjects(ctx context.Context) ([]string, error) {
// ListProjects returns the project IDs of all visible projects.
// If query is non-empty it is forwarded to the Resource Manager
// SearchProjects API which supports free-text search (AIP-160).
// Results are capped at maxProjects.
const maxProjects = 100

func (c *Client) ListProjects(ctx context.Context, query string) ([]string, error) {
projectIDs := []string{}
req := &resourcemanagerpb.SearchProjectsRequest{}
req := &resourcemanagerpb.SearchProjectsRequest{
Query: query,
PageSize: maxProjects,
}
it := c.rClient.SearchProjects(ctx, req)
for {
project, err := it.Next()
Expand All @@ -246,6 +260,9 @@ func (c *Client) ListProjects(ctx context.Context) ([]string, error) {
continue
}
projectIDs = append(projectIDs, project.ProjectId)
if len(projectIDs) >= maxProjects {
break
}
}
return projectIDs, nil
}
Expand Down
14 changes: 7 additions & 7 deletions pkg/plugin/mocks/API.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/plugin/mocks/mocks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package mocks

import "testing"

func TestMock(t *testing.T) {}
16 changes: 13 additions & 3 deletions pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -209,7 +210,7 @@ func (d *CloudTraceDatasource) CallResource(ctx context.Context, req *backend.Ca
if resource == "gceDefaultProject" {
proj, err := utils.GCEDefaultProject(ctx, "")
if err != nil {
log.DefaultLogger.Warn("problem getting GCE default project", "error", err)
log.DefaultLogger.Error("problem getting GCE default project", "error", err)
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusBadGateway,
Body: []byte(sanitizeErrorMessage(err)),
Expand All @@ -228,9 +229,18 @@ func (d *CloudTraceDatasource) CallResource(ctx context.Context, req *backend.Ca
Body: []byte(`No such path`),
})
} else {
projects, err := client.ListProjects(ctx)
reqUrl, err := url.Parse(req.URL)
if err != nil {
log.DefaultLogger.Warn("problem listing projects", "error", err)
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusBadRequest,
Body: []byte(`Invalid request URL`),
})
}
searchQuery := reqUrl.Query().Get("query")

projects, err := client.ListProjects(ctx, searchQuery)
if err != nil {
log.DefaultLogger.Error("problem listing projects", "error", err)
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusBadGateway,
Body: []byte(sanitizeErrorMessage(err)),
Expand Down
66 changes: 66 additions & 0 deletions pkg/plugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package plugin

import (
"context"
"encoding/json"
"errors"
"testing"
"time"
Expand Down Expand Up @@ -341,3 +342,68 @@ func TestSanitizeErrorMessage_GoAngleBrackets(t *testing.T) {
result := sanitizeErrorMessage(err)
require.Equal(t, "x509: cannot parse <nil> as ASN.1", result)
}

// responseSender is a test helper that captures the CallResource response
type responseSender struct {
resp *backend.CallResourceResponse
}

func (s *responseSender) Send(resp *backend.CallResourceResponse) error {
s.resp = resp
return nil
}

func TestCallResource_Projects(t *testing.T) {
expectedProjects := []string{"project-a", "project-b", "project-c"}

client := mocks.NewAPI(t)
client.On("ListProjects", mock.Anything, "").Return(expectedProjects, nil)

ds := &CloudTraceDatasource{
client: client,
}

sender := &responseSender{}
err := ds.CallResource(context.Background(), &backend.CallResourceRequest{
Path: "projects",
URL: "projects",
}, sender)

require.NoError(t, err)
require.NotNil(t, sender.resp)
require.Equal(t, 200, sender.resp.Status)

var projects []string
err = json.Unmarshal(sender.resp.Body, &projects)
require.NoError(t, err)
require.Equal(t, expectedProjects, projects)
client.AssertExpectations(t)
}

func TestCallResource_ProjectsWithQuery(t *testing.T) {
expectedProjects := []string{"proj-a"}

client := mocks.NewAPI(t)
client.On("ListProjects", mock.Anything, "proj-a").Return(expectedProjects, nil)

ds := &CloudTraceDatasource{
client: client,
}

sender := &responseSender{}
err := ds.CallResource(context.Background(), &backend.CallResourceRequest{
Path: "projects",
URL: "projects?query=proj-a",
}, sender)

require.NoError(t, err)
require.NotNil(t, sender.resp)
require.Equal(t, 200, sender.resp.Status)

var projects []string
err = json.Unmarshal(sender.resp.Body, &projects)
require.NoError(t, err)
require.Equal(t, expectedProjects, projects)
client.AssertExpectations(t)
}

Loading
Loading