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
59 changes: 59 additions & 0 deletions internal/server/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ body {
.nav-logo {
width: 90%;
margin-bottom: 20px;
flex-shrink: 0;

img {
max-width: 100%;
Expand Down Expand Up @@ -199,6 +200,11 @@ body {
}

#secondary-panel-content {
flex: 1;
overflow-y: auto;
width: 100%;
min-height: 0;

ul {
list-style: none;
padding: 0;
Expand Down Expand Up @@ -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);
}
}
}


2 changes: 1 addition & 1 deletion internal/server/static/js/loadTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<p class="error">Failed to load tools. Please try again later.</p>';
secondNavContent.innerHTML = `<p class="error">Failed to load tools: <pre><code>${error}</code></pre></p>`;
}
}

Expand Down
78 changes: 67 additions & 11 deletions internal/server/static/js/toolDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});

Expand Down Expand Up @@ -451,4 +461,50 @@ export function isParamIncluded(toolId, paramName) {

console.warn(`Include checkbox not found for ID: ${includeCheckboxId}`);
return null;
}
}

// Templates for inserting token retrieval instructions into edit header modal
const AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT = `
<p>To obtain a Google OAuth ID token using a service account:</p>
<ol>
<li>Make sure you are on the intended SERVICE account (typically contain iam.gserviceaccount.com). Verify by running the command below.
<pre><code>gcloud auth list</code></pre>
</li>
<li>Print an id token with the audience set to your clientID defined in tools file:
<pre><code>gcloud auth print-identity-token --audiences=YOUR_CLIENT_ID_HERE</code></pre>
</li>
<li>Copy the output token.</li>
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
<pre><code>{
"Content-Type": "application/json",
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
} </code></pre>
</li>
</ol>
<p>This token is typically short-lived.</p>`;

const AUTH_TOKEN_INSTRUCTIONS_STANDARD = `
<p>To obtain a Google OAuth ID token using a standard account:</p>
<ol>
<li>Make sure you are on your intended standard account. Verify by running the command below.
<pre><code>gcloud auth list</code></pre>
</li>
<li>Within your Cloud Console, add the following link to the "Authorized Redirect URIs".</li>
<pre><code>https://developers.google.com/oauthplayground</code></pre>
<li>Go to the Google OAuth Playground site: <a href="https://developers.google.com/oauthplayground/" target="_blank">https://developers.google.com/oauthplayground/</a></li>
<li>In the top right settings menu, select "Use your own OAuth Credentials".</li>
<li>Input your clientID (from tools file), along with the client secret from Cloud Console.</li>
<li>Inside the Google OAuth Playground, select "Google OAuth2 API v2.</li>
<ul>
<li>Select "Authorize APIs".</li>
<li>Select "Exchange Authorization codes for tokens"</li>
<li>Copy the id_token field provided in the response.</li>
</ul>
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
<pre><code>{
"Content-Type": "application/json",
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
} </code></pre>
</li>
</ol>
<p>This token is typically short-lived.</p>`;
51 changes: 51 additions & 0 deletions internal/server/static/js/toolsets.js
Original file line number Diff line number Diff line change
@@ -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 = '<p>Please enter a toolset name to see available tools. <br><br>To view the default toolset that consists of all tools, please select the "Tools" tab.</p>';
}
});

// 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 = '<p>Please enter a toolset name to see available tools. <br><br>To view the default toolset that consists of all tools, please select the "Tools" tab.</p>';
}
}
});
})
54 changes: 1 addition & 53 deletions internal/server/static/tools.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,56 +30,4 @@ <h4>My Tools</h4>
});
</script>
</body>
</html>

<!-- Templates for inserting token retrieval instructions into edit header modal -->
<template id="auth-token-standard-template">
<div id="auth-tab-standard" class="auth-tab-content active">
<p>To obtain a Google OAuth ID token using a standard account:</p>
<ol>
<li>Make sure you are on your intended standard account. Verify by running the command below.
<pre><code>gcloud auth list</code></pre>
</li>
<li>Within your Cloud Console, add the following link to the "Authorized Redirect URIs".</li>
<pre><code>https://developers.google.com/oauthplayground</code></pre>
<li>Go to the Google OAuth Playground site: <a href="https://developers.google.com/oauthplayground/" target="_blank">https://developers.google.com/oauthplayground/</a></li>
<li>In the top right settings menu, select "Use your own OAuth Credentials".</li>
<li>Input your clientID (from tools file), along with the client secret from Cloud Console.</li>
<li>Inside the Google OAuth Playground, select "Google OAuth2 API v2.</li>
<ul>
<li>Select "Authorize APIs".</li>
<li>Select "Exchange Authorization codes for tokens"</li>
<li>Copy the id_token field provided in the response.</li>
</ul>
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
<pre><code>{
"Content-Type": "application/json",
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
} </code></pre>
</li>
</ol>
<p>This token is typically short-lived.</p>
</div>
</template>

<template id="auth-token-service-template">
<div id="auth-tab-service" class="auth-tab-content">
<p>To obtain a Google OAuth ID token using a service account:</p>
<ol>
<li>Make sure you are on the intended SERVICE account (typically contain iam.gserviceaccount.com). Verify by running the command below.
<pre><code>gcloud auth list</code></pre>
</li>
<li>Print an id token with the audience set to your clientID defined in tools file:
<pre><code>gcloud auth print-identity-token --audiences=YOUR_CLIENT_ID_HERE</code></pre>
</li>
<li>Copy the output token.</li>
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
<pre><code>{
"Content-Type": "application/json",
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
} </code></pre>
</li>
</ol>
<p>This token is typically short-lived.</p>
</div>
</template>
</html>
41 changes: 41 additions & 0 deletions internal/server/static/toolsets.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolsets View</title>
<link rel="stylesheet" href="/ui/css/style.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

</head>
<body>
<div id="navbar-container" data-active-nav="/ui/toolsets"></div>

<aside class="second-nav">
<h4>Retrieve Toolset</h4>
<div class="search-container">
<input type="text" id="toolset-search-input" placeholder="Enter toolset name...">
<button id="toolset-search-button" aria-label="Retrieve Tools">
<span class="material-icons">search</span>
</button>
</div>
<div id="secondary-panel-content">
<p>Retrieve toolset to see available tools.</p>
</div>
</aside>

<div id="main-content-container"></div>

<script type="module" src="/ui/js/toolsets.js"></script>
<script src="/ui/js/navbar.js"></script>
<script src="/ui/js/mainContent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbarContainer = document.getElementById('navbar-container');
const activeNav = navbarContainer.getAttribute('data-active-nav');
renderNavbar('navbar-container', activeNav);
renderMainContent('main-content-container', 'tool-display-area');
});
</script>
</body>
</html>
1 change: 1 addition & 0 deletions internal/server/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
14 changes: 14 additions & 0 deletions internal/server/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading