mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-14 09:57:58 -05:00
Add `Sign in with Google` button within Toolbox UI's `Edit Header` modal that automatically retrieves a valid ID token for users based on an input clientID. This should make it significantly easier/faster for users to format request headers properly and test tools that use auth.
531 lines
21 KiB
JavaScript
531 lines
21 KiB
JavaScript
// 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 { handleRunTool, displayResults } from './runTool.js';
|
|
import { createGoogleAuthMethodItem } from './auth.js'
|
|
|
|
/**
|
|
* Helper function to create form inputs for parameters.
|
|
*/
|
|
function createParamInput(param, toolId) {
|
|
const paramItem = document.createElement('div');
|
|
paramItem.className = 'param-item';
|
|
|
|
const label = document.createElement('label');
|
|
const INPUT_ID = `param-${toolId}-${param.name}`;
|
|
const NAME_TEXT = document.createTextNode(param.name);
|
|
label.setAttribute('for', INPUT_ID);
|
|
label.appendChild(NAME_TEXT);
|
|
|
|
const IS_AUTH_PARAM = param.authServices && param.authServices.length > 0;
|
|
let additionalLabelText = '';
|
|
if (IS_AUTH_PARAM) {
|
|
additionalLabelText += ' (auth)';
|
|
}
|
|
if (!param.required) {
|
|
additionalLabelText += ' (optional)';
|
|
}
|
|
|
|
if (additionalLabelText) {
|
|
const additionalSpan = document.createElement('span');
|
|
additionalSpan.textContent = additionalLabelText;
|
|
additionalSpan.classList.add('param-label-extras');
|
|
label.appendChild(additionalSpan);
|
|
}
|
|
paramItem.appendChild(label);
|
|
|
|
const inputCheckboxWrapper = document.createElement('div');
|
|
const inputContainer = document.createElement('div');
|
|
inputCheckboxWrapper.className = 'input-checkbox-wrapper';
|
|
inputContainer.className = 'param-input-element-container';
|
|
|
|
// Build parameter's value input box.
|
|
const PLACEHOLDER_LABEL = param.label;
|
|
let inputElement;
|
|
let boolValueLabel = null;
|
|
|
|
if (param.type === 'textarea') {
|
|
inputElement = document.createElement('textarea');
|
|
inputElement.rows = 3;
|
|
inputContainer.appendChild(inputElement);
|
|
} else if(param.type === 'checkbox') {
|
|
inputElement = document.createElement('input');
|
|
inputElement.type = 'checkbox';
|
|
inputElement.title = PLACEHOLDER_LABEL;
|
|
inputElement.checked = false;
|
|
|
|
// handle true/false label for boolean params
|
|
boolValueLabel = document.createElement('span');
|
|
boolValueLabel.className = 'checkbox-bool-label';
|
|
boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
|
|
|
|
inputContainer.appendChild(inputElement);
|
|
inputContainer.appendChild(boolValueLabel);
|
|
|
|
inputElement.addEventListener('change', () => {
|
|
boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
|
|
});
|
|
} else {
|
|
inputElement = document.createElement('input');
|
|
inputElement.type = param.type;
|
|
inputContainer.appendChild(inputElement);
|
|
}
|
|
|
|
inputElement.id = INPUT_ID;
|
|
inputElement.name = param.name;
|
|
inputElement.classList.add('param-input-element');
|
|
|
|
if (IS_AUTH_PARAM) {
|
|
inputElement.disabled = true;
|
|
inputElement.classList.add('auth-param-input');
|
|
if (param.type !== 'checkbox') {
|
|
inputElement.placeholder = param.authServices;
|
|
}
|
|
} else if (param.type !== 'checkbox') {
|
|
inputElement.placeholder = PLACEHOLDER_LABEL ? PLACEHOLDER_LABEL.trim() : '';
|
|
}
|
|
inputCheckboxWrapper.appendChild(inputContainer);
|
|
|
|
// create the "Include Param" checkbox
|
|
const INCLUDE_CHECKBOX_ID = `include-${INPUT_ID}`;
|
|
const includeContainer = document.createElement('div');
|
|
const includeCheckbox = document.createElement('input');
|
|
|
|
includeContainer.className = 'include-param-container';
|
|
includeCheckbox.type = 'checkbox';
|
|
includeCheckbox.id = INCLUDE_CHECKBOX_ID;
|
|
includeCheckbox.name = `include-${param.name}`;
|
|
includeCheckbox.title = 'Include this parameter'; // Add a tooltip
|
|
|
|
// default to checked, unless it's an optional parameter
|
|
includeCheckbox.checked = param.required;
|
|
|
|
includeContainer.appendChild(includeCheckbox);
|
|
inputCheckboxWrapper.appendChild(includeContainer);
|
|
|
|
paramItem.appendChild(inputCheckboxWrapper);
|
|
|
|
// function to update UI based on checkbox state
|
|
const updateParamIncludedState = () => {
|
|
const isIncluded = includeCheckbox.checked;
|
|
if (isIncluded) {
|
|
paramItem.classList.remove('disabled-param');
|
|
if (!IS_AUTH_PARAM) {
|
|
inputElement.disabled = false;
|
|
}
|
|
if (boolValueLabel) {
|
|
boolValueLabel.classList.remove('disabled');
|
|
}
|
|
} else {
|
|
paramItem.classList.add('disabled-param');
|
|
inputElement.disabled = true;
|
|
if (boolValueLabel) {
|
|
boolValueLabel.classList.add('disabled');
|
|
}
|
|
}
|
|
};
|
|
|
|
// add event listener to the include checkbox
|
|
includeCheckbox.addEventListener('change', updateParamIncludedState);
|
|
updateParamIncludedState();
|
|
|
|
return paramItem;
|
|
}
|
|
|
|
/**
|
|
* Function to create the header editor popup modal.
|
|
* @param {string} toolId The unique identifier for the tool.
|
|
* @param {!Object<string, string>} currentHeaders The current headers.
|
|
* @param {function(!Object<string, string>): void} saveCallback A function to be
|
|
* called when the "Save" button is clicked and the headers are successfully
|
|
* parsed. The function receives the updated headers object as its argument.
|
|
* @return {!HTMLDivElement} The outermost div element of the created modal.
|
|
*/
|
|
function createHeaderEditorModal(toolId, currentHeaders, toolParameters, authRequired, saveCallback) {
|
|
const MODAL_ID = `header-modal-${toolId}`;
|
|
let modal = document.getElementById(MODAL_ID);
|
|
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
|
|
modal = document.createElement('div');
|
|
modal.id = MODAL_ID;
|
|
modal.className = 'header-modal';
|
|
|
|
const modalContent = document.createElement('div');
|
|
const modalHeader = document.createElement('h5');
|
|
const headersTextarea = document.createElement('textarea');
|
|
|
|
modalContent.className = 'header-modal-content';
|
|
modalHeader.textContent = 'Edit Request Headers';
|
|
headersTextarea.id = `headers-textarea-${toolId}`;
|
|
headersTextarea.className = 'headers-textarea';
|
|
headersTextarea.rows = 10;
|
|
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
|
|
|
|
// handle authenticated params
|
|
const authProfileNames = new Set();
|
|
toolParameters.forEach(param => {
|
|
const isAuthParam = param.authServices && param.authServices.length > 0;
|
|
if (isAuthParam && param.authServices) {
|
|
param.authServices.forEach(name => authProfileNames.add(name));
|
|
}
|
|
});
|
|
|
|
// handle authorized invocations
|
|
if (authRequired && authRequired.length > 0) {
|
|
authRequired.forEach(name => authProfileNames.add(name));
|
|
}
|
|
|
|
modalContent.appendChild(modalHeader);
|
|
modalContent.appendChild(headersTextarea);
|
|
|
|
if (authProfileNames.size > 0 || authRequired.length > 0) {
|
|
const authHelperSection = document.createElement('div');
|
|
authHelperSection.className = 'auth-helper-section';
|
|
const authList = document.createElement('div');
|
|
authList.className = 'auth-method-list';
|
|
|
|
authProfileNames.forEach(profileName => {
|
|
const authItem = createGoogleAuthMethodItem(toolId, profileName);
|
|
authList.appendChild(authItem);
|
|
});
|
|
authHelperSection.appendChild(authList);
|
|
modalContent.appendChild(authHelperSection);
|
|
}
|
|
|
|
const modalActions = document.createElement('div');
|
|
const closeButton = document.createElement('button');
|
|
const saveButton = document.createElement('button');
|
|
const authTokenDropdown = createAuthTokenInfoDropdown();
|
|
|
|
modalActions.className = 'header-modal-actions';
|
|
closeButton.textContent = 'Close';
|
|
closeButton.className = 'btn btn--closeHeaders';
|
|
closeButton.addEventListener('click', () => closeHeaderEditor(toolId));
|
|
saveButton.textContent = 'Save';
|
|
saveButton.className = 'btn btn--saveHeaders';
|
|
saveButton.addEventListener('click', () => {
|
|
try {
|
|
const updatedHeaders = JSON.parse(headersTextarea.value);
|
|
saveCallback(updatedHeaders);
|
|
closeHeaderEditor(toolId);
|
|
} catch (e) {
|
|
alert('Invalid JSON format for headers.');
|
|
console.error("Header JSON parse error:", e);
|
|
}
|
|
});
|
|
|
|
modalActions.appendChild(closeButton);
|
|
modalActions.appendChild(saveButton);
|
|
modalContent.appendChild(modalActions);
|
|
modalContent.appendChild(authTokenDropdown);
|
|
modal.appendChild(modalContent);
|
|
|
|
return modal;
|
|
}
|
|
|
|
/**
|
|
* Function to open the header popup.
|
|
*/
|
|
function openHeaderEditor(toolId) {
|
|
const modal = document.getElementById(`header-modal-${toolId}`);
|
|
if (modal) {
|
|
modal.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function to close the header popup.
|
|
*/
|
|
function closeHeaderEditor(toolId) {
|
|
const modal = document.getElementById(`header-modal-${toolId}`);
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a dropdown element showing information on how to extract Google auth tokens.
|
|
* @return {HTMLDetailsElement} The details element representing the dropdown.
|
|
*/
|
|
function createAuthTokenInfoDropdown() {
|
|
const details = document.createElement('details');
|
|
const summary = document.createElement('summary');
|
|
const content = document.createElement('div');
|
|
|
|
details.className = 'auth-token-details';
|
|
details.appendChild(summary);
|
|
summary.textContent = 'How to extract Google OAuth ID Token manually';
|
|
content.className = 'auth-token-content';
|
|
|
|
// auth instruction dropdown
|
|
const tabButtons = document.createElement('div');
|
|
const leftTab = document.createElement('button');
|
|
const rightTab = document.createElement('button');
|
|
|
|
tabButtons.className = 'auth-tab-group';
|
|
leftTab.className = 'auth-tab-picker active';
|
|
leftTab.textContent = 'With Standard Account';
|
|
leftTab.setAttribute('data-tab', 'standard');
|
|
rightTab.className = 'auth-tab-picker';
|
|
rightTab.textContent = 'With Service Account';
|
|
rightTab.setAttribute('data-tab', 'service');
|
|
|
|
tabButtons.appendChild(leftTab);
|
|
tabButtons.appendChild(rightTab);
|
|
content.appendChild(tabButtons);
|
|
|
|
const tabContentContainer = document.createElement('div');
|
|
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'));
|
|
tabContents.forEach(c => c.classList.remove('active'));
|
|
|
|
btn.classList.add('active');
|
|
|
|
const tabId = btn.getAttribute('data-tab');
|
|
const activeContent = content.querySelector(`#auth-tab-${tabId}`);
|
|
if (activeContent) {
|
|
activeContent.classList.add('active');
|
|
}
|
|
});
|
|
});
|
|
|
|
details.appendChild(content);
|
|
return details;
|
|
}
|
|
|
|
/**
|
|
* Renders the tool display area.
|
|
*/
|
|
export function renderToolInterface(tool, containerElement) {
|
|
const TOOL_ID = tool.id;
|
|
containerElement.innerHTML = '';
|
|
|
|
let lastResults = null;
|
|
let currentHeaders = {
|
|
"Content-Type": "application/json"
|
|
};
|
|
|
|
// function to update lastResults so we can toggle json
|
|
const updateLastResults = (newResults) => {
|
|
lastResults = newResults;
|
|
};
|
|
const updateCurrentHeaders = (newHeaders) => {
|
|
currentHeaders = newHeaders;
|
|
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
|
|
containerElement.appendChild(newModal);
|
|
};
|
|
|
|
const gridContainer = document.createElement('div');
|
|
gridContainer.className = 'tool-details-grid';
|
|
|
|
const toolInfoContainer = document.createElement('div');
|
|
const nameBox = document.createElement('div');
|
|
const descBox = document.createElement('div');
|
|
|
|
nameBox.className = 'tool-box tool-name';
|
|
nameBox.innerHTML = `<h5>Name:</h5><p>${tool.name}</p>`;
|
|
descBox.className = 'tool-box tool-description';
|
|
descBox.innerHTML = `<h5>Description:</h5><p>${tool.description}</p>`;
|
|
|
|
toolInfoContainer.className = 'tool-info';
|
|
toolInfoContainer.appendChild(nameBox);
|
|
toolInfoContainer.appendChild(descBox);
|
|
gridContainer.appendChild(toolInfoContainer);
|
|
|
|
const DISLCAIMER_INFO = "*Checked parameters are sent with the value from their text field. Empty fields will be sent as an empty string. To exclude a parameter, uncheck it."
|
|
const paramsContainer = document.createElement('div');
|
|
const form = document.createElement('form');
|
|
const paramsHeader = document.createElement('div');
|
|
const disclaimerText = document.createElement('div');
|
|
|
|
paramsContainer.className = 'tool-params tool-box';
|
|
paramsContainer.innerHTML = '<h5>Parameters:</h5>';
|
|
paramsHeader.className = 'params-header';
|
|
paramsContainer.appendChild(paramsHeader);
|
|
disclaimerText.textContent = DISLCAIMER_INFO;
|
|
disclaimerText.className = 'params-disclaimer';
|
|
paramsContainer.appendChild(disclaimerText);
|
|
|
|
form.id = `tool-params-form-${TOOL_ID}`;
|
|
|
|
tool.parameters.forEach(param => {
|
|
form.appendChild(createParamInput(param, TOOL_ID));
|
|
});
|
|
paramsContainer.appendChild(form);
|
|
gridContainer.appendChild(paramsContainer);
|
|
|
|
containerElement.appendChild(gridContainer);
|
|
|
|
const RESPONSE_AREA_ID = `tool-response-area-${TOOL_ID}`;
|
|
const runButtonContainer = document.createElement('div');
|
|
const editHeadersButton = document.createElement('button');
|
|
const runButton = document.createElement('button');
|
|
|
|
editHeadersButton.className = 'btn btn--editHeaders';
|
|
editHeadersButton.textContent = 'Edit Headers';
|
|
editHeadersButton.addEventListener('click', () => openHeaderEditor(TOOL_ID));
|
|
runButtonContainer.className = 'run-button-container';
|
|
runButtonContainer.appendChild(editHeadersButton);
|
|
|
|
runButton.className = 'btn btn--run';
|
|
runButton.textContent = 'Run Tool';
|
|
runButtonContainer.appendChild(runButton);
|
|
containerElement.appendChild(runButtonContainer);
|
|
|
|
// response Area (bottom)
|
|
const responseContainer = document.createElement('div');
|
|
const responseHeaderControls = document.createElement('div');
|
|
const responseHeader = document.createElement('h5');
|
|
const responseArea = document.createElement('textarea');
|
|
|
|
responseContainer.className = 'tool-response tool-box';
|
|
responseHeaderControls.className = 'response-header-controls';
|
|
responseHeader.textContent = 'Response:';
|
|
responseHeaderControls.appendChild(responseHeader);
|
|
|
|
// prettify box
|
|
const PRETTIFY_ID = `prettify-${TOOL_ID}`;
|
|
const prettifyDiv = document.createElement('div');
|
|
const prettifyLabel = document.createElement('label');
|
|
const prettifyCheckbox = document.createElement('input');
|
|
|
|
prettifyDiv.className = 'prettify-container';
|
|
prettifyLabel.setAttribute('for', PRETTIFY_ID);
|
|
prettifyLabel.textContent = 'Prettify JSON';
|
|
prettifyLabel.className = 'prettify-label';
|
|
|
|
prettifyCheckbox.type = 'checkbox';
|
|
prettifyCheckbox.id = PRETTIFY_ID;
|
|
prettifyCheckbox.checked = true;
|
|
prettifyCheckbox.className = 'prettify-checkbox';
|
|
|
|
prettifyDiv.appendChild(prettifyLabel);
|
|
prettifyDiv.appendChild(prettifyCheckbox);
|
|
|
|
responseHeaderControls.appendChild(prettifyDiv);
|
|
responseContainer.appendChild(responseHeaderControls);
|
|
|
|
responseArea.id = RESPONSE_AREA_ID;
|
|
responseArea.readOnly = true;
|
|
responseArea.placeholder = 'Results will appear here...';
|
|
responseArea.className = 'tool-response-area';
|
|
responseArea.rows = 10;
|
|
responseContainer.appendChild(responseArea);
|
|
|
|
containerElement.appendChild(responseContainer);
|
|
|
|
// create and append the header editor modal
|
|
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
|
|
containerElement.appendChild(headerModal);
|
|
|
|
prettifyCheckbox.addEventListener('change', () => {
|
|
if (lastResults) {
|
|
displayResults(lastResults, responseArea, prettifyCheckbox.checked);
|
|
}
|
|
});
|
|
|
|
runButton.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
handleRunTool(TOOL_ID, form, responseArea, tool.parameters, prettifyCheckbox, updateLastResults, currentHeaders);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks if a specific parameter is marked as included for a given tool.
|
|
* @param {string} toolId The ID of the tool.
|
|
* @param {string} paramName The name of the parameter.
|
|
* @return {boolean|null} True if the parameter's include checkbox is checked,
|
|
* False if unchecked, Null if the checkbox element is not found.
|
|
*/
|
|
export function isParamIncluded(toolId, paramName) {
|
|
const inputId = `param-${toolId}-${paramName}`;
|
|
const includeCheckboxId = `include-${inputId}`;
|
|
const includeCheckbox = document.getElementById(includeCheckboxId);
|
|
|
|
if (includeCheckbox && includeCheckbox.type === 'checkbox') {
|
|
return includeCheckbox.checked;
|
|
}
|
|
|
|
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>`; |