From fef9f417f338f4735dc41da858e8510ecea66932 Mon Sep 17 00:00:00 2001 From: Alex Talreja Date: Fri, 11 Jul 2025 22:51:50 +0000 Subject: [PATCH 1/4] feat: inspect tools from Toolbox UI --- internal/server/static/css/style.css | 139 ++++++++++++++++++++++- internal/server/static/index.html | 8 +- internal/server/static/js/toolDisplay.js | 96 ++++++++++++++++ internal/server/static/js/tools.js | 134 ++++++++++++++++++++++ internal/server/static/tools.html | 39 +++++++ internal/server/web.go | 1 + internal/server/web_test.go | 8 ++ 7 files changed, 419 insertions(+), 6 deletions(-) create mode 100644 internal/server/static/js/toolDisplay.js create mode 100644 internal/server/static/js/tools.js create mode 100644 internal/server/static/tools.html diff --git a/internal/server/static/css/style.css b/internal/server/static/css/style.css index f12b187c1284..b5163da3b6c5 100644 --- a/internal/server/static/css/style.css +++ b/internal/server/static/css/style.css @@ -4,6 +4,11 @@ body { margin: 0; font-family: 'Trebuchet MS'; background-color: #f8f9fa; + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; } .left-nav { @@ -17,6 +22,17 @@ body { align-items: center; } +.second-nav { + flex: 0 0 250px; + background-color: #fff; + box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + display: flex; + flex-direction: column; + padding: 15px; + align-items: center; +} + .nav-logo { width: 100%; margin-bottom: 20px; @@ -41,8 +57,8 @@ body { } .left-nav ul li a { - display: flex; - align-items: center; + display: flex; + align-items: center; padding: 12px; text-decoration: none; color: #333; @@ -87,3 +103,122 @@ body { flex-grow: 1; overflow-y: auto; } + +.tool-button { + display: flex; + align-items: center; + padding: 12px; + text-decoration: none; + color: #333; + background-color: transparent; + border: none; + border-radius: 0; + width: 100%; + text-align: left; + cursor: pointer; + font-family: inherit; + font-size: inherit; + + transition: background-color 0.1s ease-in-out, border-radius 0.1s ease-in-out; +} + +.tool-button:hover { + background-color: #e9e9e9; + border-radius: 35px; +} + +.tool-button:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(208, 208, 208, 0.5); +} + +.tool-button.active { + background-color: #d0d0d0; + font-weight: bold; + border-radius: 35px; +} + +.tool-button.active:hover { + background-color: #d0d0d0; +} + +#secondary-panel-content ul { + list-style: none; + padding: 0; + margin: 0; + width: 100%; +} + +.tool-details-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 20px; + margin: 0 0 20px 0; + align-items: start; +} + +.tool-info { + display: flex; + flex-direction: column; + gap: 15px; +} + +.tool-params { + background-color: #ffffff; + padding: 15px; + border-radius: 4px; + border: 1px solid #ddd; +} + +.tool-box { + background-color: #ffffff; + padding: 15px; + border-radius: 4px; + border: 1px solid #eee; +} + +.tool-box h5 { + margin-top: 0; + font-weight: bold; +} + +.param-item { + margin-bottom: 12px; +} + +.param-item label { + display: block; + margin-bottom: 4px; + font-family: inherit; +} + +.optional-label { + font-style: italic; + font-weight: lighter; + color: rgb(125, 125, 125); +} + +.param-item input[type="text"], +.param-item input[type="number"], +.param-item select, +.param-item textarea, +.param-item input[type="checkbox"] { + width: calc(100% - 12px); + padding: 6px; + border: 1px solid #ccc; + border-radius: 4px; + font-family: inherit; +} + +.tool-response { + margin: 20px 0 0 0; +} + +.tool-response textarea { + width: 100%; + min-height: 150px; + padding: 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-family: monospace; +} diff --git a/internal/server/static/index.html b/internal/server/static/index.html index a64f16ebbd59..344aeb676897 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -12,10 +12,10 @@ App Logo diff --git a/internal/server/static/js/toolDisplay.js b/internal/server/static/js/toolDisplay.js new file mode 100644 index 000000000000..f503929fc808 --- /dev/null +++ b/internal/server/static/js/toolDisplay.js @@ -0,0 +1,96 @@ +// 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 inputId = `param-${toolId}-${param.name}`; + label.setAttribute('for', inputId); + + const nameText = document.createTextNode(param.name); + label.appendChild(nameText); + if (!param.required) { + const optionalSpan = document.createElement('span'); + optionalSpan.textContent = ' (optional)'; + optionalSpan.classList.add('optional-label'); + label.appendChild(optionalSpan); + } + paramItem.appendChild(label); + + let placeholderText = param.label; + let inputElement; + if (param.type === 'textarea') { + inputElement = document.createElement('textarea'); + inputElement.rows = 3; + } else if(param.type === 'checkbox') { + inputElement = document.createElement('input'); + inputElement.type = 'checkbox'; + inputElement.title = placeholderText; + } else { + inputElement = document.createElement('input'); + inputElement.type = param.type; + } + + inputElement.id = inputId; + inputElement.name = param.name; + if (param.type !== 'checkbox') { + inputElement.placeholder = placeholderText.trim(); + } + paramItem.appendChild(inputElement); + return paramItem; +} + +// renders the tool display area +export function renderToolInterface(tool, containerElement) { + containerElement.innerHTML = ''; + const toolId = tool.id; + + const gridContainer = document.createElement('div'); + gridContainer.className = 'tool-details-grid'; + + const toolInfoContainer = document.createElement('div'); + toolInfoContainer.className = 'tool-info'; + + const nameBox = document.createElement('div'); + nameBox.className = 'tool-box tool-name'; + nameBox.innerHTML = `
Name:

${tool.name}

`; + toolInfoContainer.appendChild(nameBox); + + const descBox = document.createElement('div'); + descBox.className = 'tool-box tool-description'; + descBox.innerHTML = `
Description:

${tool.description}

`; + toolInfoContainer.appendChild(descBox); + + gridContainer.appendChild(toolInfoContainer); + + const paramsContainer = document.createElement('div'); + paramsContainer.className = 'tool-params'; + paramsContainer.innerHTML = '
Parameters:
'; + const form = document.createElement('form'); + form.id = `tool-params-form-${toolId}`; + + tool.parameters.forEach(param => { + form.appendChild(createParamInput(param, toolId)); + }); + paramsContainer.appendChild(form); + + gridContainer.appendChild(paramsContainer); + containerElement.appendChild(gridContainer); + + const responseContainer = document.createElement('div'); + responseContainer.className = 'tool-response tool-box'; + + const responseHeader = document.createElement('h5'); + responseHeader.textContent = 'Response:'; + responseContainer.appendChild(responseHeader); + + const responseAreaId = `tool-response-area-${toolId}`; + const responseArea = document.createElement('textarea'); + responseArea.id = responseAreaId; + responseArea.readOnly = true; + responseArea.placeholder = 'Results will appear here...'; + responseArea.rows = 10; + responseContainer.appendChild(responseArea); + + containerElement.appendChild(responseContainer); +} diff --git a/internal/server/static/js/tools.js b/internal/server/static/js/tools.js new file mode 100644 index 000000000000..a04b3d12b09f --- /dev/null +++ b/internal/server/static/js/tools.js @@ -0,0 +1,134 @@ +import { renderToolInterface } from "./toolDisplay.js"; + +document.addEventListener('DOMContentLoaded', () => { + const toolDisplayArea = document.getElementById('tool-display-area'); + const secondaryPanelContent = document.getElementById('secondary-panel-content'); + + if (!secondaryPanelContent || !toolDisplayArea) { + console.error('Required DOM elements not found.'); + return; + } + + // fetches tools + async function loadTools() { + secondaryPanelContent.innerHTML = '

Fetching tools...

'; + try { + const response = await fetch('/api/toolset'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const apiResponse = await response.json(); + renderToolList(apiResponse); + } catch (error) { + console.error('Failed to load tools:', error); + secondaryPanelContent.innerHTML = '

Failed to load tools. Please try again later.

'; + } + } + + // renders the fetched tools into the nav bar + function renderToolList(apiResponse) { + secondaryPanelContent.innerHTML = ''; + + if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) { + console.error('Error: Expected an object with a "tools" property, but received:', apiResponse); + secondaryPanelContent.textContent = 'Error: Invalid response format from toolset API.'; + return; + } + + const toolsObject = apiResponse.tools; + const toolNames = Object.keys(toolsObject); + + if (toolNames.length === 0) { + secondaryPanelContent.textContent = 'No tools found.'; + return; + } + + const ul = document.createElement('ul'); + toolNames.forEach(toolName => { + const li = document.createElement('li'); + const button = document.createElement('button'); + button.textContent = toolName; + button.dataset.toolname = toolName; + button.classList.add('tool-button'); + button.addEventListener('click', handleToolClick); + li.appendChild(button); + ul.appendChild(li); + }); + secondaryPanelContent.appendChild(ul); + } + + // handles selecting a specific tool from the secondary nav bar + function handleToolClick(event) { + const toolName = event.target.dataset.toolname; + if (toolName) { + const currentActive = secondaryPanelContent.querySelector('.tool-button.active'); + if (currentActive) { + currentActive.classList.remove('active'); + } + event.target.classList.add('active'); + fetchToolDetails(toolName); + } + } + + // fetches details for specific tool + async function fetchToolDetails(toolName) { + toolDisplayArea.innerHTML = '

Loading tool details...

'; + + try { + const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const apiResponse = await response.json(); + + if (!apiResponse.tools || !apiResponse.tools[toolName]) { + throw new Error(`Tool "${toolName}" data not found in API response.`); + } + const toolObject = apiResponse.tools[toolName]; + + const toolInterfaceData = { + id: toolName, + name: toolName, + description: toolObject.description || "No description provided.", + parameters: (toolObject.parameters || []).map(param => { + let inputType = 'text'; + const apiType = param.type ? param.type.toLowerCase() : 'string'; + let valueType = 'string'; + let label = param.description || param.name; + + if (apiType === 'integer' || apiType === 'number') { + inputType = 'number'; + valueType = 'number'; + } else if (apiType === 'boolean') { + inputType = 'checkbox'; + valueType = 'boolean'; + } else if (apiType === 'array') { + inputType = 'textarea'; + const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string'; + valueType = `array<${itemType}>`; + label += ' (Array)'; + } + + return { + name: param.name, + type: inputType, + valueType: valueType, + label: label, + required: param.required || false, + // defaultValue: param.default, can't do this yet bc tool manifest doesn't have default + }; + }) + }; + + console.log("Transformed toolInterfaceData:", toolInterfaceData); + + renderToolInterface(toolInterfaceData, toolDisplayArea); + } catch (error) { + console.error(`Failed to load details for tool "${toolName}":`, error); + toolDisplayArea.innerHTML = `

Failed to load details for ${toolName}. ${error.message}

`; + } + } + + // Initial load of tools list + loadTools(); +}); diff --git a/internal/server/static/tools.html b/internal/server/static/tools.html new file mode 100644 index 000000000000..0b3da854576c --- /dev/null +++ b/internal/server/static/tools.html @@ -0,0 +1,39 @@ + + + + + + Tools View + + + + + + + +
+
+
+
+

Welcome to the Main Page

+

This is the main content area. Click a tab on the left to navigate.

+
+
+ + + \ No newline at end of file diff --git a/internal/server/web.go b/internal/server/web.go index bd0a16054cd1..303436e1acd8 100644 --- a/internal/server/web.go +++ b/internal/server/web.go @@ -22,6 +22,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") }) // 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 877919435187..476367e262de 100644 --- a/internal/server/web_test.go +++ b/internal/server/web_test.go @@ -35,6 +35,14 @@ func TestWebEndpoint(t *testing.T) { wantContentType: "text/html; charset=utf-8", wantPageTitle: "Toolbox UI", }, + { + name: "web tools page GET", + method: http.MethodGet, + path: "/tools", + wantStatus: http.StatusOK, + wantContentType: "text/html; charset=utf-8", + wantPageTitle: "Tools View", + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { From e60424bf37802147695df5275dad63f14783a8f1 Mon Sep 17 00:00:00 2001 From: Alex Talreja Date: Mon, 14 Jul 2025 18:57:06 +0000 Subject: [PATCH 2/4] resolve lints --- internal/server/static/css/style.css | 16 +++++++-- internal/server/static/js/toolDisplay.js | 42 ++++++++++++++++++++---- internal/server/static/js/tools.js | 18 +++++++++- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/internal/server/static/css/style.css b/internal/server/static/css/style.css index b5163da3b6c5..9563e647cbd0 100644 --- a/internal/server/static/css/style.css +++ b/internal/server/static/css/style.css @@ -12,7 +12,7 @@ body { } .left-nav { - flex: 0 0 200px; + flex: 0 0 250px; background-color: #fff; box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15); z-index: 10; @@ -20,6 +20,8 @@ body { flex-direction: column; padding: 15px; align-items: center; + position: relative; + z-index: 3; } .second-nav { @@ -31,10 +33,12 @@ body { flex-direction: column; padding: 15px; align-items: center; + position: relative; + z-index: 2; } .nav-logo { - width: 100%; + width: 90%; margin-bottom: 20px; } @@ -178,6 +182,7 @@ body { } .tool-box h5 { + color: #4285f4ff; margin-top: 0; font-weight: bold; } @@ -192,7 +197,7 @@ body { font-family: inherit; } -.optional-label { +.param-label-extras { font-style: italic; font-weight: lighter; color: rgb(125, 125, 125); @@ -210,6 +215,11 @@ body { font-family: inherit; } +.auth-param-input { + background-color: #e0e0e0; + cursor: not-allowed; +} + .tool-response { margin: 20px 0 0 0; } diff --git a/internal/server/static/js/toolDisplay.js b/internal/server/static/js/toolDisplay.js index f503929fc808..0f5bb4d5742a 100644 --- a/internal/server/static/js/toolDisplay.js +++ b/internal/server/static/js/toolDisplay.js @@ -1,3 +1,17 @@ +// 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. + // helper function to create form inputs for parameters function createParamInput(param, toolId) { const paramItem = document.createElement('div'); @@ -9,11 +23,21 @@ function createParamInput(param, toolId) { const nameText = document.createTextNode(param.name); label.appendChild(nameText); + + const isAuthParam = param.authServices && param.authServices.length > 0; + let additionalLabelText = ''; + if (isAuthParam) { + additionalLabelText += ' (auth)'; + } if (!param.required) { - const optionalSpan = document.createElement('span'); - optionalSpan.textContent = ' (optional)'; - optionalSpan.classList.add('optional-label'); - label.appendChild(optionalSpan); + additionalLabelText += ' (optional)'; + } + + if (additionalLabelText) { + const additionalSpan = document.createElement('span'); + additionalSpan.textContent = additionalLabelText; + additionalSpan.classList.add('param-label-extras'); + label.appendChild(additionalSpan); } paramItem.appendChild(label); @@ -33,7 +57,13 @@ function createParamInput(param, toolId) { inputElement.id = inputId; inputElement.name = param.name; - if (param.type !== 'checkbox') { + if (isAuthParam) { + inputElement.disabled = true; + inputElement.classList.add('auth-param-input'); + if (param.type !== 'checkbox') { + inputElement.placeholder = param.authServices; + } + } else if (param.type !== 'checkbox') { inputElement.placeholder = placeholderText.trim(); } paramItem.appendChild(inputElement); @@ -64,7 +94,7 @@ export function renderToolInterface(tool, containerElement) { gridContainer.appendChild(toolInfoContainer); const paramsContainer = document.createElement('div'); - paramsContainer.className = 'tool-params'; + paramsContainer.className = 'tool-params tool-box'; paramsContainer.innerHTML = '
Parameters:
'; const form = document.createElement('form'); form.id = `tool-params-form-${toolId}`; diff --git a/internal/server/static/js/tools.js b/internal/server/static/js/tools.js index a04b3d12b09f..e376c094e089 100644 --- a/internal/server/static/js/tools.js +++ b/internal/server/static/js/tools.js @@ -1,3 +1,17 @@ +// 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 { renderToolInterface } from "./toolDisplay.js"; document.addEventListener('DOMContentLoaded', () => { @@ -85,6 +99,7 @@ document.addEventListener('DOMContentLoaded', () => { throw new Error(`Tool "${toolName}" data not found in API response.`); } const toolObject = apiResponse.tools[toolName]; + console.debug("Received tool object: ", toolObject) const toolInterfaceData = { id: toolName, @@ -114,13 +129,14 @@ document.addEventListener('DOMContentLoaded', () => { type: inputType, valueType: valueType, label: label, + authServices: param.authSources, required: param.required || false, // defaultValue: param.default, can't do this yet bc tool manifest doesn't have default }; }) }; - console.log("Transformed toolInterfaceData:", toolInterfaceData); + console.debug("Transformed toolInterfaceData:", toolInterfaceData); renderToolInterface(toolInterfaceData, toolDisplayArea); } catch (error) { From 59f81b8dc55482d0fd6cd71dc6846e7ffec25b31 Mon Sep 17 00:00:00 2001 From: Alex Talreja Date: Tue, 15 Jul 2025 17:33:22 +0000 Subject: [PATCH 3/4] add AbortController to prevent tool fetch race condition --- internal/server/static/js/tools.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/server/static/js/tools.js b/internal/server/static/js/tools.js index e376c094e089..77a239016595 100644 --- a/internal/server/static/js/tools.js +++ b/internal/server/static/js/tools.js @@ -23,6 +23,8 @@ document.addEventListener('DOMContentLoaded', () => { return; } + let toolDetailsAbortController = null; + // fetches tools async function loadTools() { secondaryPanelContent.innerHTML = '

Fetching tools...

'; @@ -86,10 +88,19 @@ document.addEventListener('DOMContentLoaded', () => { // fetches details for specific tool async function fetchToolDetails(toolName) { + + if (toolDetailsAbortController) { + toolDetailsAbortController.abort(); + console.debug("Aborted previous tool fetch."); + } + + toolDetailsAbortController = new AbortController(); + const signal = toolDetailsAbortController.signal; + toolDisplayArea.innerHTML = '

Loading tool details...

'; try { - const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`); + const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`, { signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -140,8 +151,12 @@ document.addEventListener('DOMContentLoaded', () => { renderToolInterface(toolInterfaceData, toolDisplayArea); } catch (error) { - console.error(`Failed to load details for tool "${toolName}":`, error); - toolDisplayArea.innerHTML = `

Failed to load details for ${toolName}. ${error.message}

`; + if (error.name === 'AbortError') { + console.debug("Previous fetch was aborted, expected behavior."); + } else { + console.error(`Failed to load details for tool "${toolName}":`, error); + toolDisplayArea.innerHTML = `

Failed to load details for ${toolName}. ${error.message}

`; + } } } From 74171737db0cb45ab2af5f07e22830b7d91221a6 Mon Sep 17 00:00:00 2001 From: Alex Talreja Date: Thu, 17 Jul 2025 23:02:30 +0000 Subject: [PATCH 4/4] resolve comments --- internal/server/static/css/style.css | 236 ++++++++++++----------- internal/server/static/index.html | 30 ++- internal/server/static/js/mainContent.js | 39 ++++ internal/server/static/js/navbar.js | 53 +++++ internal/server/static/js/toolDisplay.js | 61 +++--- internal/server/static/js/tools.js | 4 + internal/server/static/tools.html | 36 ++-- 7 files changed, 283 insertions(+), 176 deletions(-) create mode 100644 internal/server/static/js/mainContent.js create mode 100644 internal/server/static/js/navbar.js diff --git a/internal/server/static/css/style.css b/internal/server/static/css/style.css index 9563e647cbd0..98f7389c6b44 100644 --- a/internal/server/static/css/style.css +++ b/internal/server/static/css/style.css @@ -11,79 +11,94 @@ body { box-sizing: inherit; } -.left-nav { +#navbar-container { flex: 0 0 250px; - background-color: #fff; - box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15); - z-index: 10; - display: flex; - flex-direction: column; - padding: 15px; - align-items: center; + height: 100%; position: relative; - z-index: 3; + z-index: 10; } -.second-nav { - flex: 0 0 250px; - background-color: #fff; - box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15); - z-index: 10; +#main-content-container { + flex: 1; display: flex; flex-direction: column; - padding: 15px; - align-items: center; - position: relative; - z-index: 2; + min-width: 0; + overflow-x: hidden; } -.nav-logo { - width: 90%; - margin-bottom: 20px; -} - -.nav-logo img { - max-width: 100%; - height: auto; - display: block; -} - -.left-nav ul { +.left-nav { + background-color: #fff; + box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + padding: 15px; + align-items: center; + width: 100%; + height: 100%; + z-index: 3; + + ul { font-family: 'Verdana'; list-style: none; padding: 0; margin: 0; width: 100%; -} -.left-nav ul li { - margin-bottom: 5px; + li { + margin-bottom: 5px; + + a { + display: flex; + align-items: center; + padding: 12px; + text-decoration: none; + color: #333; + border-radius: 0; + + &:hover { + background-color: #e9e9e9; + border-radius: 35px; + } + + &.active { + background-color: #d0d0d0; + font-weight: bold; + border-radius: 35px; + } + } + } + } } -.left-nav ul li a { +.second-nav { + flex: 0 0 250px; + background-color: #fff; + box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15); + z-index: 2; display: flex; + flex-direction: column; + padding: 15px; align-items: center; - padding: 12px; - text-decoration: none; - color: #333; - border-radius: 0; + position: relative; } -.left-nav ul li a:hover { - background-color: #e9e9e9; - border-radius: 35px; -} +.nav-logo { + width: 90%; + margin-bottom: 20px; -.left-nav ul li a.active { - background-color: #d0d0d0; - font-weight: bold; - border-radius: 35px; + img { + max-width: 100%; + height: auto; + display: block; + } } .main-content-area { flex: 1; display: flex; flex-direction: column; + min-width: 0; + overflow-x: hidden; } .top-bar { @@ -95,17 +110,11 @@ body { border-bottom: 1px solid #eee; } -.top-bar span { - font-weight: bold; - font-size: 1.2em; - font-family: 'Trebuchet MS'; - color: #333; -} - .content { padding: 20px; flex-grow: 1; overflow-y: auto; + overflow-x: hidden; } .tool-button { @@ -124,33 +133,35 @@ body { font-size: inherit; transition: background-color 0.1s ease-in-out, border-radius 0.1s ease-in-out; -} -.tool-button:hover { - background-color: #e9e9e9; - border-radius: 35px; -} + &:hover { + background-color: #e9e9e9; + border-radius: 35px; + } -.tool-button:focus { - outline: none; - box-shadow: 0 0 0 2px rgba(208, 208, 208, 0.5); -} + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(208, 208, 208, 0.5); + } -.tool-button.active { - background-color: #d0d0d0; - font-weight: bold; - border-radius: 35px; -} + &.active { + background-color: #d0d0d0; + font-weight: bold; + border-radius: 35px; -.tool-button.active:hover { - background-color: #d0d0d0; + &:hover { + background-color: #d0d0d0; + } + } } -#secondary-panel-content ul { - list-style: none; - padding: 0; - margin: 0; - width: 100%; +#secondary-panel-content { + ul { + list-style: none; + padding: 0; + margin: 0; + width: 100%; + } } .tool-details-grid { @@ -179,56 +190,65 @@ body { padding: 15px; border-radius: 4px; border: 1px solid #eee; -} -.tool-box h5 { - color: #4285f4ff; - margin-top: 0; - font-weight: bold; + h5 { + color: #4285f4; + margin-top: 0; + font-weight: bold; + } } .param-item { margin-bottom: 12px; -} -.param-item label { - display: block; - margin-bottom: 4px; - font-family: inherit; + label { + display: block; + margin-bottom: 4px; + font-family: inherit; + } + + input[type="text"], + input[type="number"], + select, + textarea { + width: calc(100% - 12px); + padding: 6px; + border: 1px solid #ccc; + border-radius: 4px; + font-family: inherit; + } + + input[type="checkbox"] { + width: auto; + padding: 0; + border: initial; + border-radius: initial; + vertical-align: middle; + margin-right: 4px; + accent-color: #4285f4; + } } .param-label-extras { font-style: italic; - font-weight: lighter; + font-weight: lighter; color: rgb(125, 125, 125); } -.param-item input[type="text"], -.param-item input[type="number"], -.param-item select, -.param-item textarea, -.param-item input[type="checkbox"] { - width: calc(100% - 12px); - padding: 6px; - border: 1px solid #ccc; - border-radius: 4px; - font-family: inherit; -} - .auth-param-input { - background-color: #e0e0e0; + background-color: #e0e0e0; cursor: not-allowed; } .tool-response { - margin: 20px 0 0 0; -} - -.tool-response textarea { - width: 100%; - min-height: 150px; - padding: 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-family: monospace; + margin: 20px 0 0 0; + + textarea { + width: 100%; + min-height: 150px; + padding: 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-family: monospace; + } } diff --git a/internal/server/static/index.html b/internal/server/static/index.html index 344aeb676897..b152f69d6a1f 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -7,24 +7,18 @@ - + +
-
-
-
-
-

Homepage

-
-
+ + + diff --git a/internal/server/static/js/mainContent.js b/internal/server/static/js/mainContent.js new file mode 100644 index 000000000000..d1f435972e64 --- /dev/null +++ b/internal/server/static/js/mainContent.js @@ -0,0 +1,39 @@ +// 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. + +/** + * Renders the main content area into the HTML. + * @param {string} containerId The ID of the DOM element to inject the content into. + * @param {string | null} idString The id of the item inside the main content area. + */ +function renderMainContent(containerId, idString) { + const mainContentContainer = document.getElementById(containerId); + if (!mainContentContainer) { + console.error(`Content container with ID "${containerId}" not found.`); + return; + } + + const contentHTML = ` +
+
+
+
+

Welcome to MCP Toolbox UI

+

This is the main content area. Click a tab on the left to navigate.

+
+
+ `; + + mainContentContainer.innerHTML = contentHTML; +} diff --git a/internal/server/static/js/navbar.js b/internal/server/static/js/navbar.js new file mode 100644 index 000000000000..72fa6b96af87 --- /dev/null +++ b/internal/server/static/js/navbar.js @@ -0,0 +1,53 @@ +// 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. + +/** + * Renders the navigation bar HTML content into the specified container element. + * @param {string} containerId The ID of the DOM element to inject the navbar into. + * @param {string | null} activePath The active tab from the navbar. + */ +function renderNavbar(containerId, activePath) { + const navbarContainer = document.getElementById(containerId); + if (!navbarContainer) { + console.error(`Navbar container with ID "${containerId}" not found.`); + return; + } + + const navbarHTML = ` + + `; + + navbarContainer.innerHTML = navbarHTML; + if (activePath) { + const navLinks = navbarContainer.querySelectorAll('.left-nav ul li a'); + navLinks.forEach(link => { + const linkPath = new URL(link.href).pathname; + if (linkPath === activePath) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + } +} diff --git a/internal/server/static/js/toolDisplay.js b/internal/server/static/js/toolDisplay.js index 0f5bb4d5742a..f9f3b27534a4 100644 --- a/internal/server/static/js/toolDisplay.js +++ b/internal/server/static/js/toolDisplay.js @@ -12,21 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -// helper function to create form inputs for parameters +/** + * 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 inputId = `param-${toolId}-${param.name}`; - label.setAttribute('for', inputId); + const INPUT_ID = `param-${toolId}-${param.name}`; + const NAME_TEXT = document.createTextNode(param.name); + label.setAttribute('for', INPUT_ID); + label.appendChild(NAME_TEXT); - const nameText = document.createTextNode(param.name); - label.appendChild(nameText); - - const isAuthParam = param.authServices && param.authServices.length > 0; + const IS_AUTH_PARAM = param.authServices && param.authServices.length > 0; let additionalLabelText = ''; - if (isAuthParam) { + if (IS_AUTH_PARAM) { additionalLabelText += ' (auth)'; } if (!param.required) { @@ -41,7 +42,8 @@ function createParamInput(param, toolId) { } paramItem.appendChild(label); - let placeholderText = param.label; + // Build parameter's value input box. + const PLACEHOLDER_LABEL = param.label; let inputElement; if (param.type === 'textarea') { inputElement = document.createElement('textarea'); @@ -49,74 +51,75 @@ function createParamInput(param, toolId) { } else if(param.type === 'checkbox') { inputElement = document.createElement('input'); inputElement.type = 'checkbox'; - inputElement.title = placeholderText; + inputElement.title = PLACEHOLDER_LABEL; } else { inputElement = document.createElement('input'); inputElement.type = param.type; } - inputElement.id = inputId; + inputElement.id = INPUT_ID; inputElement.name = param.name; - if (isAuthParam) { + 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 = placeholderText.trim(); + inputElement.placeholder = PLACEHOLDER_LABEL.trim(); } paramItem.appendChild(inputElement); return paramItem; } -// renders the tool display area +/** + * Renders the tool display area. + */ export function renderToolInterface(tool, containerElement) { + const TOOL_ID = tool.id; containerElement.innerHTML = ''; - const toolId = tool.id; const gridContainer = document.createElement('div'); gridContainer.className = 'tool-details-grid'; const toolInfoContainer = document.createElement('div'); - toolInfoContainer.className = 'tool-info'; - const nameBox = document.createElement('div'); + const descBox = document.createElement('div'); + nameBox.className = 'tool-box tool-name'; nameBox.innerHTML = `
Name:

${tool.name}

`; - toolInfoContainer.appendChild(nameBox); - - const descBox = document.createElement('div'); descBox.className = 'tool-box tool-description'; descBox.innerHTML = `
Description:

${tool.description}

`; - toolInfoContainer.appendChild(descBox); + toolInfoContainer.className = 'tool-info'; + toolInfoContainer.appendChild(nameBox); + toolInfoContainer.appendChild(descBox); gridContainer.appendChild(toolInfoContainer); const paramsContainer = document.createElement('div'); + const form = document.createElement('form'); paramsContainer.className = 'tool-params tool-box'; paramsContainer.innerHTML = '
Parameters:
'; - const form = document.createElement('form'); - form.id = `tool-params-form-${toolId}`; + form.id = `tool-params-form-${TOOL_ID}`; tool.parameters.forEach(param => { - form.appendChild(createParamInput(param, toolId)); + 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 responseContainer = document.createElement('div'); - responseContainer.className = 'tool-response tool-box'; - const responseHeader = document.createElement('h5'); + const responseArea = document.createElement('textarea'); + responseHeader.textContent = 'Response:'; + responseContainer.className = 'tool-response tool-box'; responseContainer.appendChild(responseHeader); - const responseAreaId = `tool-response-area-${toolId}`; - const responseArea = document.createElement('textarea'); - responseArea.id = responseAreaId; + responseArea.id = RESPONSE_AREA_ID; responseArea.readOnly = true; responseArea.placeholder = 'Results will appear here...'; responseArea.rows = 10; diff --git a/internal/server/static/js/tools.js b/internal/server/static/js/tools.js index 77a239016595..a5584e151b0b 100644 --- a/internal/server/static/js/tools.js +++ b/internal/server/static/js/tools.js @@ -14,6 +14,10 @@ import { renderToolInterface } from "./toolDisplay.js"; +/** + * These functions runs after the browser finishes loading and parsing HTML structure. + * This ensures that elements can be safely accessed. + */ document.addEventListener('DOMContentLoaded', () => { const toolDisplayArea = document.getElementById('tool-display-area'); const secondaryPanelContent = document.getElementById('secondary-panel-content'); diff --git a/internal/server/static/tools.html b/internal/server/static/tools.html index 0b3da854576c..f139cf447d7a 100644 --- a/internal/server/static/tools.html +++ b/internal/server/static/tools.html @@ -4,20 +4,10 @@ Tools View - + - + -
-
-
-
-

Welcome to the Main Page

-

This is the main content area. Click a tab on the left to navigate.

-
-
- +
+ + + + + \ No newline at end of file