Skip to content

Commit 835adfc

Browse files
authored
V1.6.1 (#168)
* Update Go version to 1.24.4 in release workflow * improve project load performance * prep for 1.6.1
1 parent 12edd95 commit 835adfc

10 files changed

Lines changed: 173 additions & 100 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# Changelog
2+
## 1.6.1 (2026-03-15)
3+
* Improve the project dropdown performance
4+
* Fix a few minor issues
5+
26
## 1.6.0 (2026-03-09)
37
* Fix authentication bug where access token auth could fail (#151)
48
* Fix project dropdown only showing limited results (#144)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "googlecloud-logging-datasource",
3-
"version": "1.6.0",
3+
"version": "1.6.1",
44
"description": "Backend Grafana plugin that enables visualization of GCP Cloud Logging logs in Grafana.",
55
"scripts": {
66
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",

pkg/plugin/cloudlogging/client.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ type API interface {
4848
ListLogs(context.Context, *Query) ([]*loggingpb.LogEntry, error)
4949
// TestConnection queries for any log from the given project
5050
TestConnection(ctx context.Context, projectID string) error
51-
// ListProjects returns the project IDs of all visible projects
52-
ListProjects(context.Context) ([]string, error)
51+
// ListProjects returns the project IDs of all visible projects.
52+
// If query is non-empty it is forwarded to the Resource Manager search filter.
53+
ListProjects(ctx context.Context, query string) ([]string, error)
5354
// ListProjectBuckets returns all log buckets of a project
5455
ListProjectBuckets(ctx context.Context, projectId string) ([]string, error)
5556
// ListProjectBucketViews returns all views of a log bucket
@@ -267,10 +268,17 @@ func (q *Query) String() string {
267268
)
268269
}
269270

270-
// ListProjects returns the project IDs of all visible projects
271-
func (c *Client) ListProjects(ctx context.Context) ([]string, error) {
271+
// ListProjects returns the project IDs of all visible projects.
272+
// If query is non-empty it is forwarded to the Resource Manager search filter
273+
// (e.g. "id:*term* OR name:*term*"). Results are capped at maxProjects.
274+
const maxProjects = 100
275+
276+
func (c *Client) ListProjects(ctx context.Context, query string) ([]string, error) {
272277
projectIDs := []string{}
273-
req := &resourcemanagerpb.SearchProjectsRequest{}
278+
req := &resourcemanagerpb.SearchProjectsRequest{
279+
Query: query,
280+
PageSize: maxProjects,
281+
}
274282
it := c.rClient.SearchProjects(ctx, req)
275283
for {
276284
project, err := it.Next()
@@ -284,6 +292,9 @@ func (c *Client) ListProjects(ctx context.Context) ([]string, error) {
284292
continue
285293
}
286294
projectIDs = append(projectIDs, project.ProjectId)
295+
if len(projectIDs) >= maxProjects {
296+
break
297+
}
287298
}
288299
return projectIDs, nil
289300
}

pkg/plugin/mocks/API.go

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/plugin/plugin.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func (d *CloudLoggingDatasource) CallResource(ctx context.Context, req *backend.
219219
if resource == "gcedefaultproject" {
220220
proj, err := utils.GCEDefaultProject(ctx, "")
221221
if err != nil {
222-
log.DefaultLogger.Warn("problem getting GCE default project", "error", err)
222+
log.DefaultLogger.Error("problem getting GCE default project", "error", err)
223223
return sender.Send(&backend.CallResourceResponse{
224224
Status: http.StatusBadGateway,
225225
Body: []byte(sanitizeErrorMessage(err)),
@@ -233,9 +233,18 @@ func (d *CloudLoggingDatasource) CallResource(ctx context.Context, req *backend.
233233
})
234234
}
235235
} else if resource == "projects" {
236-
projects, err := client.ListProjects(ctx)
236+
reqUrl, err := url.Parse(req.URL)
237237
if err != nil {
238-
log.DefaultLogger.Warn("problem listing projects", "error", err)
238+
return sender.Send(&backend.CallResourceResponse{
239+
Status: http.StatusBadRequest,
240+
Body: []byte(`Invalid request URL`),
241+
})
242+
}
243+
searchQuery := reqUrl.Query().Get("query")
244+
245+
projects, err := client.ListProjects(ctx, searchQuery)
246+
if err != nil {
247+
log.DefaultLogger.Error("problem listing projects", "error", err)
239248
return sender.Send(&backend.CallResourceResponse{
240249
Status: http.StatusBadGateway,
241250
Body: []byte(sanitizeErrorMessage(err)),
@@ -262,7 +271,7 @@ func (d *CloudLoggingDatasource) CallResource(ctx context.Context, req *backend.
262271

263272
bucketNames, err := client.ListProjectBuckets(ctx, params.Get("ProjectId"))
264273
if err != nil {
265-
log.DefaultLogger.Warn("problem listing log buckets", "error", err)
274+
log.DefaultLogger.Error("problem listing log buckets", "error", err)
266275
return sender.Send(&backend.CallResourceResponse{
267276
Status: http.StatusBadGateway,
268277
Body: []byte(sanitizeErrorMessage(err)),
@@ -295,7 +304,7 @@ func (d *CloudLoggingDatasource) CallResource(ctx context.Context, req *backend.
295304

296305
views, err := client.ListProjectBucketViews(ctx, params.Get("ProjectId"), params.Get("BucketId"))
297306
if err != nil {
298-
log.DefaultLogger.Warn("problem listing log views", "error", err)
307+
log.DefaultLogger.Error("problem listing log views", "error", err)
299308
return sender.Send(&backend.CallResourceResponse{
300309
Status: http.StatusBadGateway,
301310
Body: []byte(sanitizeErrorMessage(err)),

pkg/plugin/plugin_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ func TestCallResource_Projects(t *testing.T) {
378378
expectedProjects := []string{"project-a", "project-b", "project-c", "project-d", "project-e"}
379379

380380
client := mocks.NewAPI(t)
381-
client.On("ListProjects", mock.Anything).Return(expectedProjects, nil)
381+
client.On("ListProjects", mock.Anything, "").Return(expectedProjects, nil)
382382

383383
ds := &CloudLoggingDatasource{
384384
client: client,
@@ -401,6 +401,33 @@ func TestCallResource_Projects(t *testing.T) {
401401
client.AssertExpectations(t)
402402
}
403403

404+
func TestCallResource_ProjectsWithQuery(t *testing.T) {
405+
expectedProjects := []string{"proj-a"}
406+
407+
client := mocks.NewAPI(t)
408+
client.On("ListProjects", mock.Anything, "proj-a").Return(expectedProjects, nil)
409+
410+
ds := &CloudLoggingDatasource{
411+
client: client,
412+
}
413+
414+
sender := &responseSender{}
415+
err := ds.CallResource(context.Background(), &backend.CallResourceRequest{
416+
Path: "projects",
417+
URL: "projects?query=proj-a",
418+
}, sender)
419+
420+
require.NoError(t, err)
421+
require.NotNil(t, sender.resp)
422+
require.Equal(t, 200, sender.resp.Status)
423+
424+
var projects []string
425+
err = json.Unmarshal(sender.resp.Body, &projects)
426+
require.NoError(t, err)
427+
require.Equal(t, expectedProjects, projects)
428+
client.AssertExpectations(t)
429+
}
430+
404431
func TestSanitizeErrorMessage_HTML(t *testing.T) {
405432
htmlErr := errors.New(`<html><head> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <title>502 Server Error</title> </head> <body text=#000000 bgcolor=#ffffff> <h1>Error: Server Error</h1> <h2>The server encountered a temporary error and could not complete your request.<p>Please try again in 30 seconds.</h2> <h2></h2> </body></html>`)
406433
result := sanitizeErrorMessage(htmlErr)

src/ConfigEditor.tsx

Lines changed: 67 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data';
1818
import { ConnectionConfig, GoogleAuthType } from '@grafana/google-sdk';
19-
import { Field, Label, SecretInput, Select } from '@grafana/ui';
19+
import { Checkbox, Field, Input, SecretInput, Select } from '@grafana/ui';
2020
import React, { PureComponent } from 'react';
2121
import { authTypes, CloudLoggingOptions, DataSourceSecureJsonData } from './types';
2222

@@ -97,65 +97,70 @@ export class ConfigEditor extends PureComponent<Props> {
9797
) : null}
9898
{!options.jsonData.oauthPassThru ? (
9999
<div>
100-
<input type="checkbox" onChange={() => handleClick()} checked={this.state.isChecked} /> To impersonate an
101-
existing Google Cloud service account.
102-
<div hidden={!this.state.isChecked}>
103-
<Label>Service Account:</Label>
104-
<input
105-
size={60}
106-
id="serviceAccount"
107-
value={this.state.sa}
108-
onChange={(e) => {
109-
this.setState({ sa: e.target.value }, () => {
110-
this.props.options.jsonData.serviceAccountToImpersonate = this.state.sa;
111-
});
112-
}}
113-
/>
114-
</div>
115-
</div>
116-
) : null}
117-
{options.jsonData.authenticationType === ('accessToken' as GoogleAuthType) ? (
118-
<div>
119-
<div style={{ marginTop: '10px' }}>
120-
<div className="gf-form-label__desc">
121-
Alternatively, configure a temporary access token and a project ID. This will override other
122-
authentication methods.
123-
</div>
124-
<div style={{ marginTop: '10px' }}>
125-
<Label>Access Token</Label>
126-
<SecretInput
127-
autoComplete="new-password"
128-
value={secureJsonData.accessToken || ''}
100+
<Checkbox
101+
label="To impersonate an existing Google Cloud service account."
102+
value={this.state.isChecked}
103+
onChange={() => handleClick()}
104+
onPointerEnterCapture={undefined}
105+
onPointerLeaveCapture={undefined}
106+
/>
107+
{this.state.isChecked && (
108+
<Field label="Service Account">
109+
<Input
110+
id="serviceAccount"
111+
width={60}
112+
value={this.state.sa}
129113
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
130-
onOptionsChange({
131-
...options,
132-
secureJsonData: {
133-
...secureJsonData,
134-
accessToken: e.target.value,
135-
},
136-
});
137-
}}
138-
isConfigured={!!options.secureJsonFields?.accessToken}
139-
onReset={() => {
140-
onOptionsChange({
141-
...options,
142-
secureJsonData: {
143-
...secureJsonData,
144-
accessToken: '',
145-
},
146-
secureJsonFields: {
147-
...options.secureJsonFields,
148-
accessToken: false,
149-
},
114+
this.setState({ sa: e.target.value }, () => {
115+
this.props.options.jsonData.serviceAccountToImpersonate = this.state.sa;
150116
});
151117
}}
152118
onPointerEnterCapture={undefined}
153119
onPointerLeaveCapture={undefined}
154120
/>
155-
</div>
156-
</div>
121+
</Field>
122+
)}
157123
</div>
158124
) : null}
125+
{options.jsonData.authenticationType === ('accessToken' as GoogleAuthType) ? (
126+
<>
127+
<p>
128+
Alternatively, configure a temporary access token and a project ID. This will override other
129+
authentication methods.
130+
</p>
131+
<Field label="Access Token">
132+
<SecretInput
133+
autoComplete="new-password"
134+
value={secureJsonData.accessToken || ''}
135+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
136+
onOptionsChange({
137+
...options,
138+
secureJsonData: {
139+
...secureJsonData,
140+
accessToken: e.target.value,
141+
},
142+
});
143+
}}
144+
isConfigured={!!options.secureJsonFields?.accessToken}
145+
onReset={() => {
146+
onOptionsChange({
147+
...options,
148+
secureJsonData: {
149+
...secureJsonData,
150+
accessToken: '',
151+
},
152+
secureJsonFields: {
153+
...options.secureJsonFields,
154+
accessToken: false,
155+
},
156+
});
157+
}}
158+
onPointerEnterCapture={undefined}
159+
onPointerLeaveCapture={undefined}
160+
/>
161+
</Field>
162+
</>
163+
) : null}
159164
{defaultProject(this.props)}
160165
</>
161166
);
@@ -166,9 +171,8 @@ const defaultProject = (props: Props) => {
166171
const { options, onOptionsChange } = props;
167172
return (
168173
<>
169-
<div style={{ marginTop: '10px' }}>
170-
<Label>Default Project ID (required for OAuth passthrough)</Label>
171-
<input
174+
<Field label="Default Project ID" description="Required for OAuth passthrough">
175+
<Input
172176
autoComplete="off"
173177
value={options.jsonData.defaultProject || ''}
174178
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -180,11 +184,12 @@ const defaultProject = (props: Props) => {
180184
},
181185
});
182186
}}
187+
onPointerEnterCapture={undefined}
188+
onPointerLeaveCapture={undefined}
183189
/>
184-
</div>
185-
<div style={{ marginTop: '10px' }}>
186-
<Label>Universe Domain (optional)</Label>
187-
<input
190+
</Field>
191+
<Field label="Universe Domain" description="Optional">
192+
<Input
188193
autoComplete="off"
189194
placeholder="googleapis.com (default)"
190195
value={options.jsonData.universeDomain || ''}
@@ -197,8 +202,10 @@ const defaultProject = (props: Props) => {
197202
},
198203
});
199204
}}
205+
onPointerEnterCapture={undefined}
206+
onPointerLeaveCapture={undefined}
200207
/>
201-
</div>
208+
</Field>
202209
</>
203210
);
204211
};

0 commit comments

Comments
 (0)