diff --git a/internal/server/static/css/style.css b/internal/server/static/css/style.css index e93421a8deeb..019c2a09f09a 100644 --- a/internal/server/static/css/style.css +++ b/internal/server/static/css/style.css @@ -93,6 +93,7 @@ body { .nav-logo { width: 90%; margin-bottom: 20px; + flex-shrink: 0; img { max-width: 100%; @@ -199,6 +200,11 @@ body { } #secondary-panel-content { + flex: 1; + overflow-y: auto; + width: 100%; + min-height: 0; + ul { list-style: none; padding: 0; @@ -519,3 +525,56 @@ body { font-family: monospace; } } + +.search-container { + display: flex; + width: 100%; + margin-bottom: 15px; + + #toolset-search-input { + flex-grow: 1; + padding: 10px 12px; + border: 1px solid #ccc; + border-radius: 20px 0 0 20px; + border-right: none; + font-family: inherit; + font-size: 0.9em; + color: var(--text-primary-gray); + + &:focus { + outline: none; + border-color: var(--toolbox-blue); + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3); + } + + &::placeholder { + color: var(--text-secondary-gray); + } + } + + #toolset-search-button { + padding: 10px 15px; + border: 1px solid var(--button-primary); + background-color: var(--button-primary); + color: white; + border-radius: 0 20px 20px 0; + cursor: pointer; + font-family: inherit; + font-size: 0.9em; + font-weight: bold; + transition: opacity 0.2s ease-in-out; + flex-shrink: 0; + line-height: 1; + + &:hover { + opacity: 0.8; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3); + } + } +} + + diff --git a/internal/server/static/js/loadTools.js b/internal/server/static/js/loadTools.js index 3dd0fcb5aadb..d7c11a387eca 100644 --- a/internal/server/static/js/loadTools.js +++ b/internal/server/static/js/loadTools.js @@ -34,7 +34,7 @@ export async function loadTools(secondNavContent, toolDisplayArea, toolsetName) renderToolList(apiResponse, secondNavContent, toolDisplayArea); } catch (error) { console.error('Failed to load tools:', error); - secondNavContent.innerHTML = '

Failed to load tools. Please try again later.

'; + secondNavContent.innerHTML = `

Failed to load tools:

${error}

`; } } diff --git a/internal/server/static/js/toolDisplay.js b/internal/server/static/js/toolDisplay.js index b20ff1560dfa..8fe9f66a74ed 100644 --- a/internal/server/static/js/toolDisplay.js +++ b/internal/server/static/js/toolDisplay.js @@ -267,27 +267,37 @@ function createAuthTokenInfoDropdown() { content.appendChild(tabButtons); const tabContentContainer = document.createElement('div'); - const standardTemplate = document.getElementById('auth-token-standard-template'); - const standardAccount = document.importNode(standardTemplate.content, true).firstElementChild; - const serviceTemplate = document.getElementById('auth-token-service-template'); - const serviceAccount = document.importNode(serviceTemplate.content, true).firstElementChild; - - tabContentContainer.appendChild(standardAccount); - tabContentContainer.appendChild(serviceAccount); + const standardAccInstructions = document.createElement('div'); + const serviceAccInstructions = document.createElement('div'); + + standardAccInstructions.id = 'auth-tab-standard'; + standardAccInstructions.className = 'auth-tab-content active'; + standardAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_STANDARD; + serviceAccInstructions.id = 'auth-tab-service'; + serviceAccInstructions.className = 'auth-tab-content'; + serviceAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT; + + tabContentContainer.appendChild(standardAccInstructions); + tabContentContainer.appendChild(serviceAccInstructions); content.appendChild(tabContentContainer); // switching tabs logic const tabBtns = [leftTab, rightTab]; + const tabContents = [standardAccInstructions, serviceAccInstructions]; + tabBtns.forEach(btn => { btn.addEventListener('click', () => { // deactivate all buttons and contents tabBtns.forEach(b => b.classList.remove('active')); - content.querySelectorAll('.auth-tab-content').forEach(c => c.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); - // activate clicked button and corresponding content btn.classList.add('active'); + const tabId = btn.getAttribute('data-tab'); - content.querySelector(`#auth-tab-${tabId}`).classList.add('active'); + const activeContent = content.querySelector(`#auth-tab-${tabId}`); + if (activeContent) { + activeContent.classList.add('active'); + } }); }); @@ -451,4 +461,50 @@ export function isParamIncluded(toolId, paramName) { console.warn(`Include checkbox not found for ID: ${includeCheckboxId}`); return null; -} \ No newline at end of file +} + +// Templates for inserting token retrieval instructions into edit header modal +const AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT = ` +

To obtain a Google OAuth ID token using a service account:

+
    +
  1. Make sure you are on the intended SERVICE account (typically contain iam.gserviceaccount.com). Verify by running the command below. +
    gcloud auth list
    +
  2. +
  3. Print an id token with the audience set to your clientID defined in tools file: +
    gcloud auth print-identity-token --audiences=YOUR_CLIENT_ID_HERE
    +
  4. +
  5. Copy the output token.
  6. +
  7. Paste this token into the header in JSON editor. The key should be the name of your auth service followed by _token +
    {
    +  "Content-Type": "application/json",
    +  "my-google-auth_token": "YOUR_ID_TOKEN_HERE"
    +}               
    +
  8. +
+

This token is typically short-lived.

`; + +const AUTH_TOKEN_INSTRUCTIONS_STANDARD = ` +

To obtain a Google OAuth ID token using a standard account:

+
    +
  1. Make sure you are on your intended standard account. Verify by running the command below. +
    gcloud auth list
    +
  2. +
  3. Within your Cloud Console, add the following link to the "Authorized Redirect URIs".
  4. +
    https://developers.google.com/oauthplayground
    +
  5. Go to the Google OAuth Playground site: https://developers.google.com/oauthplayground/
  6. +
  7. In the top right settings menu, select "Use your own OAuth Credentials".
  8. +
  9. Input your clientID (from tools file), along with the client secret from Cloud Console.
  10. +
  11. Inside the Google OAuth Playground, select "Google OAuth2 API v2.
  12. + +
  13. Paste this token into the header in JSON editor. The key should be the name of your auth service followed by _token +
    {
    +  "Content-Type": "application/json",
    +  "my-google-auth_token": "YOUR_ID_TOKEN_HERE"
    +}               
    +
  14. +
+

This token is typically short-lived.

`; \ No newline at end of file diff --git a/internal/server/static/js/toolsets.js b/internal/server/static/js/toolsets.js new file mode 100644 index 000000000000..f49afbf5f3ef --- /dev/null +++ b/internal/server/static/js/toolsets.js @@ -0,0 +1,51 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { loadTools } from "./loadTools.js"; + +document.addEventListener('DOMContentLoaded', () => { + const searchInput = document.getElementById('toolset-search-input'); + const searchButton = document.getElementById('toolset-search-button'); + const secondNavContent = document.getElementById('secondary-panel-content'); + const toolDisplayArea = document.getElementById('tool-display-area'); + + if (!searchInput || !searchButton || !secondNavContent || !toolDisplayArea) { + console.error('Required DOM elements not found.'); + return; + } + + // Event listener for search button click + searchButton.addEventListener('click', () => { + toolDisplayArea.innerHTML = ''; + const toolsetName = searchInput.value.trim(); + if (toolsetName) { + loadTools(secondNavContent, toolDisplayArea, toolsetName) + } else { + secondNavContent.innerHTML = '

Please enter a toolset name to see available tools.

To view the default toolset that consists of all tools, please select the "Tools" tab.

'; + } + }); + + // Event listener for Enter key in search input + searchInput.addEventListener('keypress', (event) => { + toolDisplayArea.innerHTML = ''; + if (event.key === 'Enter') { + const toolsetName = searchInput.value.trim(); + if (toolsetName) { + loadTools(secondNavContent, toolDisplayArea, toolsetName); + } else { + secondNavContent.innerHTML = '

Please enter a toolset name to see available tools.

To view the default toolset that consists of all tools, please select the "Tools" tab.

'; + } + } + }); +}) \ No newline at end of file diff --git a/internal/server/static/tools.html b/internal/server/static/tools.html index af20072cfc4d..f139cf447d7a 100644 --- a/internal/server/static/tools.html +++ b/internal/server/static/tools.html @@ -30,56 +30,4 @@

My Tools

}); - - - - - - + \ No newline at end of file diff --git a/internal/server/static/toolsets.html b/internal/server/static/toolsets.html new file mode 100644 index 000000000000..51da67bdf742 --- /dev/null +++ b/internal/server/static/toolsets.html @@ -0,0 +1,41 @@ + + + + + + Toolsets View + + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/internal/server/web.go b/internal/server/web.go index 303436e1acd8..23f7de06f2b1 100644 --- a/internal/server/web.go +++ b/internal/server/web.go @@ -23,6 +23,7 @@ func webRouter() (chi.Router, error) { // direct routes for html pages to provide clean URLs r.Get("/", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/index.html") }) r.Get("/tools", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/tools.html") }) + r.Get("/toolsets", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/toolsets.html") }) // handler for all other static files/assets staticFS, _ := fs.Sub(staticContent, "static") diff --git a/internal/server/web_test.go b/internal/server/web_test.go index c285d9f00044..64c173285e73 100644 --- a/internal/server/web_test.go +++ b/internal/server/web_test.go @@ -59,6 +59,20 @@ func TestWebEndpoint(t *testing.T) { wantContentType: "text/html", wantPageTitle: "Tools View", }, + { + name: "web toolsets page", + path: "/ui/toolsets", + wantStatus: http.StatusOK, + wantContentType: "text/html", + wantPageTitle: "Toolsets View", + }, + { + name: "web toolsets page with trailing slash", + path: "/ui/toolsets/", + wantStatus: http.StatusOK, + wantContentType: "text/html", + wantPageTitle: "Toolsets View", + }, } for _, tc := range testCases {