From eb793398cd1cc4006d9808ccda5dc7aea5e92bd5 Mon Sep 17 00:00:00 2001 From: "Dr. Strangelove" Date: Tue, 6 Jan 2026 12:30:20 -0500 Subject: [PATCH] feat(tools/looker): add ability to set destination folder with make_look and make_dashboard. (#2245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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> --- .../tools/looker/looker-make-dashboard.md | 4 ++- .../tools/looker/looker-make-look.md | 3 ++- .../lookermakedashboard.go | 25 ++++++++++++------- .../looker/lookermakelook/lookermakelook.go | 24 ++++++++++++------ tests/looker/looker_integration_test.go | 14 +++++++++++ 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/en/resources/tools/looker/looker-make-dashboard.md b/docs/en/resources/tools/looker/looker-make-dashboard.md index 048d42bef0..f8112bcd5d 100644 --- a/docs/en/resources/tools/looker/looker-make-dashboard.md +++ b/docs/en/resources/tools/looker/looker-make-dashboard.md @@ -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 diff --git a/docs/en/resources/tools/looker/looker-make-look.md b/docs/en/resources/tools/looker/looker-make-look.md index 148f245532..9c69898437 100644 --- a/docs/en/resources/tools/looker/looker-make-look.md +++ b/docs/en/resources/tools/looker/looker-make-look.md @@ -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 diff --git a/internal/tools/looker/lookermakedashboard/lookermakedashboard.go b/internal/tools/looker/lookermakedashboard/lookermakedashboard.go index 2930d6e993..ea64b8b148 100644 --- a/internal/tools/looker/lookermakedashboard/lookermakedashboard.go +++ b/internal/tools/looker/lookermakedashboard/lookermakedashboard.go @@ -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 { diff --git a/internal/tools/looker/lookermakelook/lookermakelook.go b/internal/tools/looker/lookermakelook/lookermakelook.go index 7244c5d6fe..f3a09805e2 100644 --- a/internal/tools/looker/lookermakelook/lookermakelook.go +++ b/internal/tools/looker/lookermakelook/lookermakelook.go @@ -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 { diff --git a/tests/looker/looker_integration_test.go b/tests/looker/looker_integration_test.go index 06ee5c0277..d179cf0483 100644 --- a/tests/looker/looker_integration_test.go +++ b/tests/looker/looker_integration_test.go @@ -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", + }, }, }, },