feat(tools/looker): add ability to set destination folder with make_look and make_dashboard. (#2245)

## Description

When running with a service account, the user has no personal folder id.
This allows a destination
folder to be specified as part of the call to make_dashboard and
make_look. If a folder is not specified
the user's personal folder will be used.

## PR Checklist

> Thank you for opening a Pull Request! Before submitting your PR, there
are a
> few things you can do to make sure it goes smoothly:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #2225

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Dr. Strangelove
2026-01-06 12:30:20 -05:00
committed by GitHub
parent cf0fc515b5
commit eb793398cd
5 changed files with 52 additions and 18 deletions

View File

@@ -18,9 +18,11 @@ It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-make-dashboard` takes one parameter:
`looker-make-dashboard` takes three parameters:
1. the `title`
2. the `description`
3. an optional `folder` id. If not provided, the user's default folder will be used.
## Example

View File

@@ -18,7 +18,7 @@ It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-make-look` takes eleven parameters:
`looker-make-look` takes twelve parameters:
1. the `model`
2. the `explore`
@@ -31,6 +31,7 @@ It's compatible with the following sources:
9. an optional `vis_config`
10. the `title`
11. an optional `description`
12. an optional `folder` id. If not provided, the user's default folder will be used.
## Example

View File

@@ -76,6 +76,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
params = append(params, titleParameter)
descParameter := parameters.NewStringParameterWithDefault("description", "", "The description of the Dashboard")
params = append(params, descParameter)
folderParameter := parameters.NewStringParameterWithDefault("folder", "", "The folder id where the Dashboard will be created. Leave blank to use the user's personal folder")
params = append(params, folderParameter)
annotations := cfg.Annotations
if annotations == nil {
@@ -130,21 +132,26 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}
paramsMap := params.AsMap()
title := paramsMap["title"].(string)
description := paramsMap["description"].(string)
folder := paramsMap["folder"].(string)
mrespFields := "id,personal_folder_id"
mresp, err := sdk.Me(mrespFields, source.LookerApiSettings())
if err != nil {
return nil, fmt.Errorf("error making me request: %s", err)
}
paramsMap := params.AsMap()
title := paramsMap["title"].(string)
description := paramsMap["description"].(string)
if mresp.PersonalFolderId == nil || *mresp.PersonalFolderId == "" {
return nil, fmt.Errorf("user does not have a personal folder. cannot continue")
if folder == "" {
if mresp.PersonalFolderId == nil || *mresp.PersonalFolderId == "" {
return nil, fmt.Errorf("user does not have a personal folder. A folder must be specified")
}
folder = *mresp.PersonalFolderId
}
dashs, err := sdk.FolderDashboards(*mresp.PersonalFolderId, "title", source.LookerApiSettings())
dashs, err := sdk.FolderDashboards(folder, "title", source.LookerApiSettings())
if err != nil {
return nil, fmt.Errorf("error getting existing dashboards in folder: %s", err)
}
@@ -155,13 +162,13 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
if slices.Contains(dashTitles, title) {
lt, _ := json.Marshal(dashTitles)
return nil, fmt.Errorf("title %s already used in user's folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt))
return nil, fmt.Errorf("title %s already used in folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt))
}
wd := v4.WriteDashboard{
Title: &title,
Description: &description,
FolderId: mresp.PersonalFolderId,
FolderId: &folder,
}
resp, err := sdk.CreateDashboard(wd, source.LookerApiSettings())
if err != nil {

View File

@@ -76,6 +76,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
params = append(params, titleParameter)
descParameter := parameters.NewStringParameterWithDefault("description", "", "The description of the Look")
params = append(params, descParameter)
folderParameter := parameters.NewStringParameterWithDefault("folder", "", "The folder id where the Look will be created. Leave blank to use the user's personal folder")
params = append(params, folderParameter)
vizParameter := parameters.NewMapParameterWithDefault("vis_config",
map[string]any{},
"The visualization config for the query",
@@ -140,17 +142,26 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
if err != nil {
return nil, fmt.Errorf("error getting sdk: %w", err)
}
paramsMap := params.AsMap()
title := paramsMap["title"].(string)
description := paramsMap["description"].(string)
folder := paramsMap["folder"].(string)
visConfig := paramsMap["vis_config"].(map[string]any)
mrespFields := "id,personal_folder_id"
mresp, err := sdk.Me(mrespFields, source.LookerApiSettings())
if err != nil {
return nil, fmt.Errorf("error making me request: %s", err)
}
paramsMap := params.AsMap()
title := paramsMap["title"].(string)
description := paramsMap["description"].(string)
if folder == "" {
if mresp.PersonalFolderId == nil || *mresp.PersonalFolderId == "" {
return nil, fmt.Errorf("user does not have a personal folder. A folder must be specified")
}
folder = *mresp.PersonalFolderId
}
looks, err := sdk.FolderLooks(*mresp.PersonalFolderId, "title", source.LookerApiSettings())
looks, err := sdk.FolderLooks(folder, "title", source.LookerApiSettings())
if err != nil {
return nil, fmt.Errorf("error getting existing looks in folder: %s", err)
}
@@ -161,10 +172,9 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
}
if slices.Contains(lookTitles, title) {
lt, _ := json.Marshal(lookTitles)
return nil, fmt.Errorf("title %s already used in user's folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt))
return nil, fmt.Errorf("title %s already used in folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt))
}
visConfig := paramsMap["vis_config"].(map[string]any)
wq.VisConfig = &visConfig
qrespFields := "id"
@@ -178,7 +188,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
UserId: mresp.Id,
Description: &description,
QueryId: qresp.Id,
FolderId: mresp.PersonalFolderId,
FolderId: &folder,
}
resp, err := sdk.CreateLook(wlwq, "", source.LookerApiSettings())
if err != nil {

View File

@@ -799,6 +799,13 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The folder id where the Look will be created. Leave blank to use the user's personal folder",
"name": "folder",
"required": false,
"type": "string",
},
map[string]any{
"additionalProperties": true,
"authSources": []any{},
@@ -869,6 +876,13 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The folder id where the Dashboard will be created. Leave blank to use the user's personal folder",
"name": "folder",
"required": false,
"type": "string",
},
},
},
},